Completed
Push — master ( 6b2386...2bf093 )
by Ahmad
04:44
created

titanscssc::lib_mix()   C

Complexity

Conditions 7
Paths 32

Size

Total Lines 32
Code Lines 21

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 32
rs 6.7273
cc 7
eloc 21
nc 32
nop 1
1
<?php
2
/**
3
 * SCSS compiler written in PHP
4
 *
5
 * @copyright 2012-2014 Leaf Corcoran
6
 *
7
 * @license http://opensource.org/licenses/gpl-license GPL-3.0
8
 * @license http://opensource.org/licenses/MIT MIT
9
 *
10
 * @link http://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 `scss_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 `scss_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 `scss_parser` class is only concerned with parsing its input.
35
 *
36
 * The `scss_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 titanscssc {
46
	static public $VERSION = 'v0.0.15';
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
	protected $registeredVars = array();
93
94
	protected $numberPrecision = 5;
95
96
	protected $formatter = 'titanscss_formatter_nested';
97
98
	/**
99
	 * Compile scss
100
	 *
101
	 * @param string $code
102
	 * @param string $name
103
	 *
104
	 * @return string
105
	 */
106
	public function compile($code, $name = null)
107
	{
108
		$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...
109
		$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...
110
		$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...
111
		$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...
112
		$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...
113
		$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...
114
		$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...
115
116
		$locale = setlocale(LC_NUMERIC, 0);
117
		setlocale(LC_NUMERIC, 'C');
118
119
		$this->parser = new titanscss_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...
120
121
		$tree = $this->parser->parse($code);
122
123
		$this->formatter = new $this->formatter();
124
125
		$this->pushEnv($tree);
126
		$this->injectVariables($this->registeredVars);
127
		$this->compileRoot($tree);
128
		$this->popEnv();
129
130
		$out = $this->formatter->format($this->scope);
131
132
		setlocale(LC_NUMERIC, $locale);
133
134
		return $out;
135
	}
136
137
	protected function isSelfExtend($target, $origin) {
138
		foreach ($origin as $sel) {
139
			if (in_array($target, $sel)) {
140
				return true;
141
			}
142
		}
143
144
		return false;
145
	}
146
147
	protected function pushExtends($target, $origin) {
148
		if ($this->isSelfExtend($target, $origin)) {
149
			return;
150
		}
151
152
		$i = count($this->extends);
153
		$this->extends[] = array($target, $origin);
154
155
		foreach ($target as $part) {
156
			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...
157
				$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...
158
			} else {
159
				$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...
160
			}
161
		}
162
	}
163
164
	protected function makeOutputBlock($type, $selectors = null) {
165
		$out = new stdClass;
166
		$out->type = $type;
167
		$out->lines = array();
168
		$out->children = array();
169
		$out->parent = $this->scope;
170
		$out->selectors = $selectors;
171
		$out->depth = $this->env->depth;
172
173
		return $out;
174
	}
175
176
	protected function matchExtendsSingle($single, &$outOrigin) {
177
		$counts = array();
178
		foreach ($single as $part) {
179
			if (!is_string($part)) return false; // hmm
180
181
			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...
182
				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...
183
					$counts[$idx] =
184
						isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
185
				}
186
			}
187
		}
188
189
		$outOrigin = array();
190
		$found = false;
191
192
		foreach ($counts as $idx => $count) {
193
			list($target, $origin) = $this->extends[$idx];
194
195
			// check count
196
			if ($count != count($target)) continue;
197
198
			// check if target is subset of single
199
			if (array_diff(array_intersect($single, $target), $target)) continue;
200
201
			$rem = array_diff($single, $target);
202
203
			foreach ($origin as $j => $new) {
204
				// prevent infinite loop when target extends itself
205
				foreach ($new as $new_selector) {
206
					if (!array_diff($single, $new_selector)) {
207
						continue 2;
208
					}
209
				}
210
211
				$origin[$j][count($origin[$j]) - 1] = $this->combineSelectorSingle(end($new), $rem);
212
			}
213
214
			$outOrigin = array_merge($outOrigin, $origin);
215
216
			$found = true;
217
		}
218
219
		return $found;
220
	}
221
222
	protected function combineSelectorSingle($base, $other) {
223
		$tag = null;
224
		$out = array();
225
226
		foreach (array($base, $other) as $single) {
227
			foreach ($single as $part) {
228
				if (preg_match('/^[^\[.#:]/', $part)) {
229
					$tag = $part;
230
				} else {
231
					$out[] = $part;
232
				}
233
			}
234
		}
235
236
		if ($tag) {
237
			array_unshift($out, $tag);
238
		}
239
240
		return $out;
241
	}
242
243
	protected function matchExtends($selector, &$out, $from = 0, $initial=true) {
244
		foreach ($selector as $i => $part) {
245
			if ($i < $from) continue;
246
247
			if ($this->matchExtendsSingle($part, $origin)) {
248
				$before = array_slice($selector, 0, $i);
249
				$after = array_slice($selector, $i + 1);
250
251
				foreach ($origin as $new) {
252
					$k = 0;
253
254
					// remove shared parts
255
					if ($initial) {
256
						foreach ($before as $k => $val) {
257
							if (!isset($new[$k]) || $val != $new[$k]) {
258
								break;
259
							}
260
						}
261
					}
262
263
					$result = array_merge(
264
						$before,
265
						$k > 0 ? array_slice($new, $k) : $new,
266
						$after);
267
268
269
					if ($result == $selector) continue;
270
					$out[] = $result;
271
272
					// recursively check for more matches
273
					$this->matchExtends($result, $out, $i, false);
274
275
					// selector sequence merging
276
					if (!empty($before) && count($new) > 1) {
277
						$result2 = array_merge(
278
							array_slice($new, 0, -1),
279
							$k > 0 ? array_slice($before, $k) : $before,
280
							array_slice($new, -1),
281
							$after);
282
283
						$out[] = $result2;
284
					}
285
				}
286
			}
287
		}
288
	}
289
290
	protected function flattenSelectors($block, $parentKey = null) {
291
		if ($block->selectors) {
292
			$selectors = array();
293
			foreach ($block->selectors as $s) {
294
				$selectors[] = $s;
295
				if (!is_array($s)) continue;
296
				// check extends
297
				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...
298
					$this->matchExtends($s, $selectors);
299
				}
300
			}
301
302
			$block->selectors = array();
303
			$placeholderSelector = false;
304
			foreach ($selectors as $selector) {
305
				if ($this->hasSelectorPlaceholder($selector)) {
306
					$placeholderSelector = true;
307
					continue;
308
				}
309
				$block->selectors[] = $this->compileSelector($selector);
310
			}
311
312
			if ($placeholderSelector && 0 == count($block->selectors) && null != $parentKey) {
313
				unset($block->parent->children[$parentKey]);
314
				return;
315
			}
316
		}
317
318
		foreach ($block->children as $key => $child) {
319
			$this->flattenSelectors($child, $key);
320
		}
321
	}
322
323
	protected function compileRoot($rootBlock)
324
	{
325
		$this->scope = $this->makeOutputBlock('root');
326
327
		$this->compileChildren($rootBlock->children, $this->scope);
328
		$this->flattenSelectors($this->scope);
329
	}
330
331
	protected function compileMedia($media) {
332
		$this->pushEnv($media);
333
334
		$mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
335
336
		if (!empty($mediaQuery)) {
337
338
			$this->scope = $this->makeOutputBlock('media', array($mediaQuery));
339
340
			$parentScope = $this->mediaParent($this->scope);
341
342
			$parentScope->children[] = $this->scope;
343
344
			// top level properties in a media cause it to be wrapped
345
			$needsWrap = false;
346
			foreach ($media->children as $child) {
347
				$type = $child[0];
348
				if ($type != 'block' && $type != 'media' && $type != 'directive') {
349
					$needsWrap = true;
350
					break;
351
				}
352
			}
353
354
			if ($needsWrap) {
355
				$wrapped = (object)array(
356
					'selectors' => array(),
357
					'children' => $media->children
358
				);
359
				$media->children = array(array('block', $wrapped));
360
			}
361
362
			$this->compileChildren($media->children, $this->scope);
363
364
			$this->scope = $this->scope->parent;
365
		}
366
367
		$this->popEnv();
368
	}
369
370
	protected function mediaParent($scope) {
371
		while (!empty($scope->parent)) {
372
			if (!empty($scope->type) && $scope->type != 'media') {
373
				break;
374
			}
375
			$scope = $scope->parent;
376
		}
377
378
		return $scope;
379
	}
380
381
	// TODO refactor compileNestedBlock and compileMedia into same thing
382
	protected function compileNestedBlock($block, $selectors) {
383
		$this->pushEnv($block);
384
385
		$this->scope = $this->makeOutputBlock($block->type, $selectors);
386
		$this->scope->parent->children[] = $this->scope;
387
		$this->compileChildren($block->children, $this->scope);
388
389
		$this->scope = $this->scope->parent;
390
		$this->popEnv();
391
	}
392
393
	/**
394
	 * Recursively compiles a block.
395
	 *
396
	 * A block is analogous to a CSS block in most cases. A single SCSS document
397
	 * is encapsulated in a block when parsed, but it does not have parent tags
398
	 * so all of its children appear on the root level when compiled.
399
	 *
400
	 * Blocks are made up of selectors and children.
401
	 *
402
	 * The children of a block are just all the blocks that are defined within.
403
	 *
404
	 * Compiling the block involves pushing a fresh environment on the stack,
405
	 * and iterating through the props, compiling each one.
406
	 *
407
	 * @see scss::compileChild()
408
	 *
409
	 * @param \StdClass $block
410
	 */
411
	protected function compileBlock($block) {
412
		$env = $this->pushEnv($block);
413
414
		$env->selectors =
415
			array_map(array($this, 'evalSelector'), $block->selectors);
416
417
		$out = $this->makeOutputBlock(null, $this->multiplySelectors($env));
418
		$this->scope->children[] = $out;
419
		$this->compileChildren($block->children, $out);
420
421
		$this->popEnv();
422
	}
423
424
	// root level comment
425
	protected function compileComment($block) {
426
		$out = $this->makeOutputBlock('comment');
427
		$out->lines[] = $block[1];
428
		$this->scope->children[] = $out;
429
	}
430
431
	// joins together .classes and #ids
432
	protected function flattenSelectorSingle($single) {
433
		$joined = array();
434
		foreach ($single as $part) {
435
			if (empty($joined) ||
436
				!is_string($part) ||
437
				preg_match('/[\[.:#%]/', $part))
438
			{
439
				$joined[] = $part;
440
				continue;
441
			}
442
443
			if (is_array(end($joined))) {
444
				$joined[] = $part;
445
			} else {
446
				$joined[count($joined) - 1] .= $part;
447
			}
448
		}
449
450
		return $joined;
451
	}
452
453
	// replaces all the interpolates
454
	protected function evalSelector($selector) {
455
		return array_map(array($this, 'evalSelectorPart'), $selector);
456
	}
457
458
	protected function evalSelectorPart($piece) {
459
		foreach ($piece as &$p) {
460
			if (!is_array($p)) continue;
461
462
			switch ($p[0]) {
463
			case 'interpolate':
464
				$p = $this->compileValue($p);
465
				break;
466
			case 'string':
467
				$p = $this->compileValue($p);
468
				break;
469
			}
470
		}
471
472
		return $this->flattenSelectorSingle($piece);
473
	}
474
475
	// compiles to string
476
	// self(&) should have been replaced by now
477
	protected function compileSelector($selector) {
478
		if (!is_array($selector)) return $selector; // media and the like
479
480
		return implode(' ', array_map(
481
			array($this, 'compileSelectorPart'), $selector));
482
	}
483
484
	protected function compileSelectorPart($piece) {
485
		foreach ($piece as &$p) {
486
			if (!is_array($p)) continue;
487
488
			switch ($p[0]) {
489
			case 'self':
490
				$p = '&';
491
				break;
492
			default:
493
				$p = $this->compileValue($p);
494
				break;
495
			}
496
		}
497
498
		return implode($piece);
499
	}
500
501
	protected function hasSelectorPlaceholder($selector)
502
	{
503
		if (!is_array($selector)) return false;
504
505
		foreach ($selector as $parts) {
506
			foreach ($parts as $part) {
507
				if ('%' == $part[0]) {
508
					return true;
509
				}
510
			}
511
		}
512
513
		return false;
514
	}
515
516
	protected function compileChildren($stms, $out) {
517
		foreach ($stms as $stm) {
518
			$ret = $this->compileChild($stm, $out);
519
			if (isset($ret)) return $ret;
520
		}
521
	}
522
523
	protected function compileMediaQuery($queryList) {
524
		$out = '@media';
525
		$first = true;
526
		foreach ($queryList as $query){
527
			$type = null;
528
			$parts = array();
529
			foreach ($query as $q) {
530
				switch ($q[0]) {
531
					case 'mediaType':
532
						if ($type) {
533
							$type = $this->mergeMediaTypes($type, array_map(array($this, 'compileValue'), array_slice($q, 1)));
534
							if (empty($type)) { // merge failed
535
								return null;
536
							}
537
						} else {
538
							$type = array_map(array($this, 'compileValue'), array_slice($q, 1));
539
						}
540
						break;
541
					case 'mediaExp':
542
						if (isset($q[2])) {
543
							$parts[] = '('. $this->compileValue($q[1]) . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ')';
544
						} else {
545
							$parts[] = '(' . $this->compileValue($q[1]) . ')';
546
						}
547
						break;
548
				}
549
			}
550
			if ($type) {
551
				array_unshift($parts, implode(' ', array_filter($type)));
552
			}
553
			if (!empty($parts)) {
554
				if ($first) {
555
					$first = false;
556
					$out .= ' ';
557
				} else {
558
					$out .= $this->formatter->tagSeparator;
559
				}
560
				$out .= implode(' and ', $parts);
561
			}
562
		}
563
		return $out;
564
	}
565
566
	protected function mergeMediaTypes($type1, $type2) {
567
		if (empty($type1)) {
568
			return $type2;
569
		}
570
		if (empty($type2)) {
571
			return $type1;
572
		}
573
		$m1 = '';
574
		$t1 = '';
575
		if (count($type1) > 1) {
576
			$m1= strtolower($type1[0]);
577
			$t1= strtolower($type1[1]);
578
		} else {
579
			$t1 = strtolower($type1[0]);
580
		}
581
		$m2 = '';
582
		$t2 = '';
583
		if (count($type2) > 1) {
584
			$m2 = strtolower($type2[0]);
585
			$t2 = strtolower($type2[1]);
586
		} else {
587
			$t2 = strtolower($type2[0]);
588
		}
589
		if (($m1 == 'not') ^ ($m2 == 'not')) {
590
			if ($t1 == $t2) {
591
				return null;
592
			}
593
			return array(
594
				$m1 == 'not' ? $m2 : $m1,
595
				$m1 == 'not' ? $t2 : $t1
596
			);
597
		} elseif ($m1 == 'not' && $m2 == 'not') {
598
			# CSS has no way of representing "neither screen nor print"
599
			if ($t1 != $t2) {
600
				return null;
601
			}
602
			return array('not', $t1);
603
		} elseif ($t1 != $t2) {
604
			return null;
605
		} else { // t1 == t2, neither m1 nor m2 are "not"
606
			return array(empty($m1)? $m2 : $m1, $t1);
607
		}
608
	}
609
610
	// returns true if the value was something that could be imported
611
	protected function compileImport($rawPath, $out) {
612
		if ($rawPath[0] == 'string') {
613
			$path = $this->compileStringContent($rawPath);
614
			if ($path = $this->findImport($path)) {
615
				$this->importFile($path, $out);
616
				return true;
617
			}
618
			return false;
619
		}
620
		if ($rawPath[0] == 'list') {
621
			// handle a list of strings
622
			if (count($rawPath[2]) == 0) return false;
623
			foreach ($rawPath[2] as $path) {
624
				if ($path[0] != 'string') return false;
625
			}
626
627
			foreach ($rawPath[2] as $path) {
628
				$this->compileImport($path, $out);
629
			}
630
631
			return true;
632
		}
633
634
		return false;
635
	}
636
637
	// return a value to halt execution
638
	protected function compileChild($child, $out) {
639
		$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...
640
		$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...
641
642
		switch ($child[0]) {
643
		case 'import':
644
			list(,$rawPath) = $child;
645
			$rawPath = $this->reduce($rawPath);
646
			if (!$this->compileImport($rawPath, $out)) {
647
				$out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
648
			}
649
			break;
650
		case 'directive':
651
			list(, $directive) = $child;
652
			$s = '@' . $directive->name;
653
			if (!empty($directive->value)) {
654
				$s .= ' ' . $this->compileValue($directive->value);
655
			}
656
			$this->compileNestedBlock($directive, array($s));
657
			break;
658
		case 'media':
659
			$this->compileMedia($child[1]);
660
			break;
661
		case 'block':
662
			$this->compileBlock($child[1]);
663
			break;
664
		case 'charset':
665
			$out->lines[] = '@charset '.$this->compileValue($child[1]).';';
666
			break;
667
		case 'assign':
668
			list(,$name, $value) = $child;
669
			if ($name[0] == 'var') {
670
				$isDefault = !empty($child[3]);
671
672
				if ($isDefault) {
673
					$existingValue = $this->get($name[1], true);
674
					$shouldSet = $existingValue == true || $existingValue == self::$null;
675
				}
676
677
				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...
678
					$this->set($name[1], $this->reduce($value));
679
				}
680
				break;
681
			}
682
683
			// if the value reduces to null from something else then
684
			// the property should be discarded
685
			if ($value[0] != 'null') {
686
				$value = $this->reduce($value);
687
				if ($value[0] == 'null') {
688
					break;
689
				}
690
			}
691
692
			$compiledValue = $this->compileValue($value);
693
			$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...
694
				$this->compileValue($name),
695
				$compiledValue);
696
			break;
697
		case 'comment':
698
			if ($out->type == 'root') {
699
				$this->compileComment($child);
700
				break;
701
			}
702
703
			$out->lines[] = $child[1];
704
			break;
705
		case 'mixin':
706
		case 'function':
707
			list(,$block) = $child;
708
			$this->set(self::$namespaces[$block->type] . $block->name, $block);
709
			break;
710
		case 'extend':
711
			list(, $selectors) = $child;
712
			foreach ($selectors as $sel) {
713
				// only use the first one
714
				$sel = current($this->evalSelector($sel));
715
				$this->pushExtends($sel, $out->selectors);
716
			}
717
			break;
718
		case 'if':
719
			list(, $if) = $child;
720
			if ($this->isTruthy($this->reduce($if->cond, true))) {
721
				return $this->compileChildren($if->children, $out);
722
			} else {
723
				foreach ($if->cases as $case) {
724
					if ($case->type == 'else' ||
725
						$case->type == 'elseif' && $this->isTruthy($this->reduce($case->cond)))
726
					{
727
						return $this->compileChildren($case->children, $out);
728
					}
729
				}
730
			}
731
			break;
732
		case 'return':
733
			return $this->reduce($child[1], true);
734
		case 'each':
735
			list(,$each) = $child;
736
			$list = $this->coerceList($this->reduce($each->list));
737
			foreach ($list[2] as $item) {
738
				$this->pushEnv();
739
				$this->set($each->var, $item);
740
				// TODO: allow return from here
741
				$this->compileChildren($each->children, $out);
742
				$this->popEnv();
743
			}
744
			break;
745
		case 'while':
746
			list(,$while) = $child;
747
			while ($this->isTruthy($this->reduce($while->cond, true))) {
748
				$ret = $this->compileChildren($while->children, $out);
749
				if ($ret) return $ret;
750
			}
751
			break;
752
		case 'for':
753
			list(,$for) = $child;
754
			$start = $this->reduce($for->start, true);
755
			$start = $start[1];
756
			$end = $this->reduce($for->end, true);
757
			$end = $end[1];
758
			$d = $start < $end ? 1 : -1;
759
760
			while (true) {
761
				if ((!$for->until && $start - $d == $end) ||
762
					($for->until && $start == $end))
763
				{
764
					break;
765
				}
766
767
				$this->set($for->var, array('number', $start, ''));
768
				$start += $d;
769
770
				$ret = $this->compileChildren($for->children, $out);
771
				if ($ret) return $ret;
772
			}
773
774
			break;
775
		case 'nestedprop':
776
			list(,$prop) = $child;
777
			$prefixed = array();
778
			$prefix = $this->compileValue($prop->prefix) . '-';
779
			foreach ($prop->children as $child) {
780
				if ($child[0] == 'assign') {
781
					array_unshift($child[1][2], $prefix);
782
				}
783
				if ($child[0] == 'nestedprop') {
784
					array_unshift($child[1]->prefix[2], $prefix);
785
				}
786
				$prefixed[] = $child;
787
			}
788
			$this->compileChildren($prefixed, $out);
789
			break;
790
		case 'include': // including a mixin
791
			list(,$name, $argValues, $content) = $child;
792
			$mixin = $this->get(self::$namespaces['mixin'] . $name, false);
793
			if (!$mixin) {
794
				$this->throwError("Undefined mixin $name");
795
			}
796
797
			$callingScope = $this->env;
798
799
			// push scope, apply args
800
			$this->pushEnv();
801
			if ($this->env->depth > 0) {
802
				$this->env->depth--;
803
			}
804
805
			if (isset($content)) {
806
				$content->scope = $callingScope;
807
				$this->setRaw(self::$namespaces['special'] . 'content', $content);
808
			}
809
810
			if (isset($mixin->args)) {
811
				$this->applyArguments($mixin->args, $argValues);
812
			}
813
814
			foreach ($mixin->children as $child) {
815
				$this->compileChild($child, $out);
816
			}
817
818
			$this->popEnv();
819
820
			break;
821
		case 'mixin_content':
822
			$content = $this->get(self::$namespaces['special'] . 'content');
823
			if (!isset($content)) {
824
				$this->throwError('Expected @content inside of mixin');
825
			}
826
827
			$strongTypes = array('include', 'block', 'for', 'while');
828
			foreach ($content->children as $child) {
829
				$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...
830
					? null
831
					: $content->scope;
832
833
				$this->compileChild($child, $out);
834
			}
835
836
			unset($this->storeEnv);
837
			break;
838
		case 'debug':
839
			list(,$value, $pos) = $child;
840
			$line = $this->parser->getLineNo($pos);
841
			$value = $this->compileValue($this->reduce($value, true));
842
			fwrite(STDERR, "Line $line DEBUG: $value\n");
843
			break;
844
		default:
845
			$this->throwError("unknown child type: $child[0]");
846
		}
847
	}
848
849
	protected function expToString($exp) {
850
		list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
851
		$content = array($this->reduce($left));
852
		if ($whiteLeft) $content[] = ' ';
853
		$content[] = $op;
854
		if ($whiteRight) $content[] = ' ';
855
		$content[] = $this->reduce($right);
856
		return array('string', '', $content);
857
	}
858
859
	protected function isTruthy($value) {
860
		return $value != self::$false && $value != self::$null;
861
	}
862
863
	// should $value cause its operand to eval
864
	protected function shouldEval($value) {
865
		switch ($value[0]) {
866
		case 'exp':
867
			if ($value[1] == '/') {
868
				return $this->shouldEval($value[2], $value[3]);
0 ignored issues
show
Unused Code introduced by
The call to titanscssc::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...
869
			}
870
		case 'var':
871
		case 'fncall':
872
			return true;
873
		}
874
		return false;
875
	}
876
877
	protected function reduce($value, $inExp = false) {
878
		list($type) = $value;
879
		switch ($type) {
880
			case 'exp':
881
				list(, $op, $left, $right, $inParens) = $value;
882
				$opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
883
884
				$inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
885
886
				$left = $this->reduce($left, true);
887
				$right = $this->reduce($right, true);
888
889
				// only do division in special cases
890
				if ($opName == 'div' && !$inParens && !$inExp) {
891
					if ($left[0] != 'color' && $right[0] != 'color') {
892
						return $this->expToString($value);
893
					}
894
				}
895
896
				$left = $this->coerceForExpression($left);
897
				$right = $this->coerceForExpression($right);
898
899
				$ltype = $left[0];
900
				$rtype = $right[0];
901
902
				// this tries:
903
				// 1. op_[op name]_[left type]_[right type]
904
				// 2. op_[left type]_[right type] (passing the op as first arg
905
				// 3. op_[op name]
906
				$fn = "op_${opName}_${ltype}_${rtype}";
907
				if (is_callable(array($this, $fn)) ||
908
					(($fn = "op_${ltype}_${rtype}") &&
909
						is_callable(array($this, $fn)) &&
910
						$passOp = true) ||
911
					(($fn = "op_${opName}") &&
912
						is_callable(array($this, $fn)) &&
913
						$genOp = true))
914
				{
915
					$unitChange = false;
916
					if (!isset($genOp) &&
917
						$left[0] == 'number' && $right[0] == 'number')
918
					{
919
						if ($opName == 'mod' && $right[2] != '') {
920
							$this->throwError("Cannot modulo by a number with units: $right[1]$right[2].");
921
						}
922
923
						$unitChange = true;
924
						$emptyUnit = $left[2] == '' || $right[2] == '';
925
						$targetUnit = '' != $left[2] ? $left[2] : $right[2];
926
927
						if ($opName != 'mul') {
928
							$left[2] = '' != $left[2] ? $left[2] : $targetUnit;
929
							$right[2] = '' != $right[2] ? $right[2] : $targetUnit;
930
						}
931
932
						if ($opName != 'mod') {
933
							$left = $this->normalizeNumber($left);
934
							$right = $this->normalizeNumber($right);
935
						}
936
937
						if ($opName == 'div' && !$emptyUnit && $left[2] == $right[2]) {
938
							$targetUnit = '';
939
						}
940
941
						if ($opName == 'mul') {
942
							$left[2] = '' != $left[2] ? $left[2] : $right[2];
943
							$right[2] = '' != $right[2] ? $right[2] : $left[2];
944
						} elseif ($opName == 'div' && $left[2] == $right[2]) {
945
							$left[2] = '';
946
							$right[2] = '';
947
						}
948
					}
949
950
					$shouldEval = $inParens || $inExp;
951
					if (isset($passOp)) {
952
						$out = $this->$fn($op, $left, $right, $shouldEval);
953
					} else {
954
						$out = $this->$fn($left, $right, $shouldEval);
955
					}
956
957
					if (isset($out)) {
958
						if ($unitChange && $out[0] == 'number') {
959
							$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...
960
						}
961
						return $out;
962
					}
963
				}
964
965
				return $this->expToString($value);
966
			case 'unary':
967
				list(, $op, $exp, $inParens) = $value;
968
				$inExp = $inExp || $this->shouldEval($exp);
969
970
				$exp = $this->reduce($exp);
971
				if ($exp[0] == 'number') {
972
					switch ($op) {
973
					case '+':
974
						return $exp;
975
					case '-':
976
						$exp[1] *= -1;
977
						return $exp;
978
					}
979
				}
980
981
				if ($op == 'not') {
982
					if ($inExp || $inParens) {
983
						if ($exp == self::$false) {
984
							return self::$true;
985
						} else {
986
							return self::$false;
987
						}
988
					} else {
989
						$op = $op . ' ';
990
					}
991
				}
992
993
				return array('string', '', array($op, $exp));
994
			case 'var':
995
				list(, $name) = $value;
996
				return $this->reduce($this->get($name));
997
			case 'list':
998
				foreach ($value[2] as &$item) {
999
					$item = $this->reduce($item);
1000
				}
1001
				return $value;
1002
			case 'string':
1003
				foreach ($value[2] as &$item) {
1004
					if (is_array($item)) {
1005
						$item = $this->reduce($item);
1006
					}
1007
				}
1008
				return $value;
1009
			case 'interpolate':
1010
				$value[1] = $this->reduce($value[1]);
1011
				return $value;
1012
			case 'fncall':
1013
				list(,$name, $argValues) = $value;
1014
1015
				// user defined function?
1016
				$func = $this->get(self::$namespaces['function'] . $name, false);
1017
				if ($func) {
1018
					$this->pushEnv();
1019
1020
					// set the args
1021
					if (isset($func->args)) {
1022
						$this->applyArguments($func->args, $argValues);
1023
					}
1024
1025
					// throw away lines and children
1026
					$tmp = (object)array(
1027
						'lines' => array(),
1028
						'children' => array()
1029
					);
1030
					$ret = $this->compileChildren($func->children, $tmp);
1031
					$this->popEnv();
1032
1033
					return !isset($ret) ? self::$defaultValue : $ret;
1034
				}
1035
1036
				// built in function
1037
				if ($this->callBuiltin($name, $argValues, $returnValue)) {
1038
					return $returnValue;
1039
				}
1040
1041
				// need to flatten the arguments into a list
1042
				$listArgs = array();
1043
				foreach ((array)$argValues as $arg) {
1044
					if (empty($arg[0])) {
1045
						$listArgs[] = $this->reduce($arg[1]);
1046
					}
1047
				}
1048
				return array('function', $name, array('list', ',', $listArgs));
1049
			default:
1050
				return $value;
1051
		}
1052
	}
1053
1054
	public function normalizeValue($value) {
1055
		$value = $this->coerceForExpression($this->reduce($value));
1056
		list($type) = $value;
1057
1058
		switch ($type) {
1059
		case 'list':
1060
			$value = $this->extractInterpolation($value);
1061
			if ($value[0] != 'list') {
1062
				return array('keyword', $this->compileValue($value));
1063
			}
1064
			foreach ($value[2] as $key => $item) {
1065
				$value[2][$key] = $this->normalizeValue($item);
1066
			}
1067
			return $value;
1068
		case 'number':
1069
			return $this->normalizeNumber($value);
1070
		default:
1071
			return $value;
1072
		}
1073
	}
1074
1075
	// just does physical lengths for now
1076
	protected function normalizeNumber($number) {
1077
		list(, $value, $unit) = $number;
1078
		if (isset(self::$unitTable['in'][$unit])) {
1079
			$conv = self::$unitTable['in'][$unit];
1080
			return array('number', $value / $conv, 'in');
1081
		}
1082
		return $number;
1083
	}
1084
1085
	// $number should be normalized
1086
	protected function coerceUnit($number, $unit) {
1087
		list(, $value, $baseUnit) = $number;
1088
		if (isset(self::$unitTable[$baseUnit][$unit])) {
1089
			$value = $value * self::$unitTable[$baseUnit][$unit];
1090
		}
1091
1092
		return array('number', $value, $unit);
1093
	}
1094
1095
	protected function op_add_number_number($left, $right) {
1096
		return array('number', $left[1] + $right[1], $left[2]);
1097
	}
1098
1099
	protected function op_mul_number_number($left, $right) {
1100
		return array('number', $left[1] * $right[1], $left[2]);
1101
	}
1102
1103
	protected function op_sub_number_number($left, $right) {
1104
		return array('number', $left[1] - $right[1], $left[2]);
1105
	}
1106
1107
	protected function op_div_number_number($left, $right) {
1108
		return array('number', $left[1] / $right[1], $left[2]);
1109
	}
1110
1111
	protected function op_mod_number_number($left, $right) {
1112
		return array('number', $left[1] % $right[1], $left[2]);
1113
	}
1114
1115
	// adding strings
1116
	protected function op_add($left, $right) {
1117
		if ($strLeft = $this->coerceString($left)) {
1118
			if ($right[0] == 'string') {
1119
				$right[1] = '';
1120
			}
1121
			$strLeft[2][] = $right;
1122
			return $strLeft;
1123
		}
1124
1125
		if ($strRight = $this->coerceString($right)) {
1126
			if ($left[0] == 'string') {
1127
				$left[1] = '';
1128
			}
1129
			array_unshift($strRight[2], $left);
1130
			return $strRight;
1131
		}
1132
	}
1133
1134
	protected function op_and($left, $right, $shouldEval) {
1135
		if (!$shouldEval) return;
1136
		if ($left != self::$false) return $right;
1137
		return $left;
1138
	}
1139
1140
	protected function op_or($left, $right, $shouldEval) {
1141
		if (!$shouldEval) return;
1142
		if ($left != self::$false) return $left;
1143
		return $right;
1144
	}
1145
1146
	protected function op_color_color($op, $left, $right) {
1147
		$out = array('color');
1148
		foreach (range(1, 3) as $i) {
1149
			$lval = isset($left[$i]) ? $left[$i] : 0;
1150
			$rval = isset($right[$i]) ? $right[$i] : 0;
1151
			switch ($op) {
1152
			case '+':
1153
				$out[] = $lval + $rval;
1154
				break;
1155
			case '-':
1156
				$out[] = $lval - $rval;
1157
				break;
1158
			case '*':
1159
				$out[] = $lval * $rval;
1160
				break;
1161
			case '%':
1162
				$out[] = $lval % $rval;
1163
				break;
1164
			case '/':
1165
				if ($rval == 0) {
1166
					$this->throwError("color: Can't divide by zero");
1167
				}
1168
				$out[] = $lval / $rval;
1169
				break;
1170
			case '==':
1171
				return $this->op_eq($left, $right);
1172
			case '!=':
1173
				return $this->op_neq($left, $right);
1174
			default:
1175
				$this->throwError("color: unknown op $op");
1176
			}
1177
		}
1178
1179
		if (isset($left[4])) $out[4] = $left[4];
1180
		elseif (isset($right[4])) $out[4] = $right[4];
1181
1182
		return $this->fixColor($out);
1183
	}
1184
1185
	protected function op_color_number($op, $left, $right) {
1186
		$value = $right[1];
1187
		return $this->op_color_color($op, $left,
1188
			array('color', $value, $value, $value));
1189
	}
1190
1191
	protected function op_number_color($op, $left, $right) {
1192
		$value = $left[1];
1193
		return $this->op_color_color($op,
1194
			array('color', $value, $value, $value), $right);
1195
	}
1196
1197
	protected function op_eq($left, $right) {
1198
		if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
1199
			$lStr[1] = '';
1200
			$rStr[1] = '';
1201
			return $this->toBool($this->compileValue($lStr) == $this->compileValue($rStr));
1202
		}
1203
1204
		return $this->toBool($left == $right);
1205
	}
1206
1207
	protected function op_neq($left, $right) {
1208
		return $this->toBool($left != $right);
1209
	}
1210
1211
	protected function op_gte_number_number($left, $right) {
1212
		return $this->toBool($left[1] >= $right[1]);
1213
	}
1214
1215
	protected function op_gt_number_number($left, $right) {
1216
		return $this->toBool($left[1] > $right[1]);
1217
	}
1218
1219
	protected function op_lte_number_number($left, $right) {
1220
		return $this->toBool($left[1] <= $right[1]);
1221
	}
1222
1223
	protected function op_lt_number_number($left, $right) {
1224
		return $this->toBool($left[1] < $right[1]);
1225
	}
1226
1227
	public function toBool($thing) {
1228
		return $thing ? self::$true : self::$false;
1229
	}
1230
1231
	/**
1232
	 * Compiles a primitive value into a CSS property value.
1233
	 *
1234
	 * Values in scssphp are typed by being wrapped in arrays, their format is
1235
	 * typically:
1236
	 *
1237
	 *     array(type, contents [, additional_contents]*)
1238
	 *
1239
	 * The input is expected to be reduced. This function will not work on
1240
	 * things like expressions and variables.
1241
	 *
1242
	 * @param array $value
1243
	 */
1244
	protected function compileValue($value) {
1245
		$value = $this->reduce($value);
1246
1247
		list($type) = $value;
1248
		switch ($type) {
1249
		case 'keyword':
1250
			return $value[1];
1251
		case 'color':
1252
			// [1] - red component (either number for a %)
1253
			// [2] - green component
1254
			// [3] - blue component
1255
			// [4] - optional alpha component
1256
			list(, $r, $g, $b) = $value;
1257
1258
			$r = round($r);
1259
			$g = round($g);
1260
			$b = round($b);
1261
1262
			if (count($value) == 5 && $value[4] != 1) { // rgba
1263
				return 'rgba('.$r.', '.$g.', '.$b.', '.$value[4].')';
1264
			}
1265
1266
			$h = sprintf('#%02x%02x%02x', $r, $g, $b);
1267
1268
			// Converting hex color to short notation (e.g. #003399 to #039)
1269
			if ($h[1] == $h[2] && $h[3] == $h[4] && $h[5] == $h[6]) {
1270
				$h = '#' . $h[1] . $h[3] . $h[5];
1271
			}
1272
1273
			return $h;
1274
		case 'number':
1275
			return round($value[1], $this->numberPrecision) . $value[2];
1276
		case 'string':
1277
			return $value[1] . $this->compileStringContent($value) . $value[1];
1278
		case 'function':
1279
			$args = !empty($value[2]) ? $this->compileValue($value[2]) : '';
1280
			return "$value[1]($args)";
1281
		case 'list':
1282
			$value = $this->extractInterpolation($value);
1283
			if ($value[0] != 'list') return $this->compileValue($value);
1284
1285
			list(, $delim, $items) = $value;
1286
1287
			$filtered = array();
1288
			foreach ($items as $item) {
1289
				if ($item[0] == 'null') continue;
1290
				$filtered[] = $this->compileValue($item);
1291
			}
1292
1293
			return implode("$delim ", $filtered);
1294
		case 'interpolated': # node created by extractInterpolation
1295
			list(, $interpolate, $left, $right) = $value;
1296
			list(,, $whiteLeft, $whiteRight) = $interpolate;
1297
1298
			$left = count($left[2]) > 0 ?
1299
				$this->compileValue($left).$whiteLeft : '';
1300
1301
			$right = count($right[2]) > 0 ?
1302
				$whiteRight.$this->compileValue($right) : '';
1303
1304
			return $left.$this->compileValue($interpolate).$right;
1305
1306
		case 'interpolate': # raw parse node
1307
			list(, $exp) = $value;
1308
1309
			// strip quotes if it's a string
1310
			$reduced = $this->reduce($exp);
1311
			switch ($reduced[0]) {
1312
				case 'string':
1313
					$reduced = array('keyword',
1314
						$this->compileStringContent($reduced));
1315
					break;
1316
				case 'null':
1317
					$reduced = array('keyword', '');
1318
			}
1319
1320
			return $this->compileValue($reduced);
1321
		case 'null':
1322
			return 'null';
1323
		default:
1324
			$this->throwError("unknown value type: $type");
1325
		}
1326
	}
1327
1328
	protected function compileStringContent($string) {
1329
		$parts = array();
1330
		foreach ($string[2] as $part) {
1331
			if (is_array($part)) {
1332
				$parts[] = $this->compileValue($part);
1333
			} else {
1334
				$parts[] = $part;
1335
			}
1336
		}
1337
1338
		return implode($parts);
1339
	}
1340
1341
	// doesn't need to be recursive, compileValue will handle that
1342
	protected function extractInterpolation($list) {
1343
		$items = $list[2];
1344
		foreach ($items as $i => $item) {
1345
			if ($item[0] == 'interpolate') {
1346
				$before = array('list', $list[1], array_slice($items, 0, $i));
1347
				$after = array('list', $list[1], array_slice($items, $i + 1));
1348
				return array('interpolated', $item, $before, $after);
1349
			}
1350
		}
1351
		return $list;
1352
	}
1353
1354
	// find the final set of selectors
1355
	protected function multiplySelectors($env) {
1356
		$envs = array();
1357
		while (null != $env) {
1358
			if (!empty($env->selectors)) {
1359
				$envs[] = $env;
1360
			}
1361
			$env = $env->parent;
1362
		};
1363
1364
		$selectors = array();
1365
		$parentSelectors = array(array());
1366
		while ($env = array_pop($envs)) {
1367
			$selectors = array();
1368
			foreach ($env->selectors as $selector) {
1369
				foreach ($parentSelectors as $parent) {
1370
					$selectors[] = $this->joinSelectors($parent, $selector);
1371
				}
1372
			}
1373
			$parentSelectors = $selectors;
1374
		}
1375
1376
		return $selectors;
1377
	}
1378
1379
	// looks for & to replace, or append parent before child
1380
	protected function joinSelectors($parent, $child) {
1381
		$setSelf = false;
1382
		$out = array();
1383
		foreach ($child as $part) {
1384
			$newPart = array();
1385
			foreach ($part as $p) {
1386
				if ($p == self::$selfSelector) {
1387
					$setSelf = true;
1388
					foreach ($parent as $i => $parentPart) {
1389
						if ($i > 0) {
1390
							$out[] = $newPart;
1391
							$newPart = array();
1392
						}
1393
1394
						foreach ($parentPart as $pp) {
1395
							$newPart[] = $pp;
1396
						}
1397
					}
1398
				} else {
1399
					$newPart[] = $p;
1400
				}
1401
			}
1402
1403
			$out[] = $newPart;
1404
		}
1405
1406
		return $setSelf ? $out : array_merge($parent, $child);
1407
	}
1408
1409
	protected function multiplyMedia($env, $childQueries = null) {
1410
		if (!isset($env) ||
1411
			!empty($env->block->type) && $env->block->type != 'media')
1412
		{
1413
			return $childQueries;
1414
		}
1415
1416
		// plain old block, skip
1417
		if (empty($env->block->type)) {
1418
			return $this->multiplyMedia($env->parent, $childQueries);
1419
		}
1420
1421
		$parentQueries = $env->block->queryList;
1422
		if ($childQueries == null) {
1423
			$childQueries = $parentQueries;
1424
		} else {
1425
			$originalQueries = $childQueries;
1426
			$childQueries = array();
1427
1428
			foreach ($parentQueries as $parentQuery){
1429
				foreach ($originalQueries as $childQuery) {
1430
					$childQueries []= array_merge($parentQuery, $childQuery);
1431
				}
1432
			}
1433
		}
1434
1435
		return $this->multiplyMedia($env->parent, $childQueries);
1436
	}
1437
1438
	// convert something to list
1439
	protected function coerceList($item, $delim = ',') {
1440
		if (isset($item) && $item[0] == 'list') {
1441
			return $item;
1442
		}
1443
1444
		return array('list', $delim, !isset($item) ? array(): array($item));
1445
	}
1446
1447
	protected function applyArguments($argDef, $argValues) {
1448
		$storeEnv = $this->getStoreEnv();
1449
1450
		$env = new stdClass;
1451
		$env->store = $storeEnv->store;
1452
1453
		$hasVariable = false;
1454
		$args = array();
1455
		foreach ($argDef as $i => $arg) {
1456
			list($name, $default, $isVariable) = $argDef[$i];
1457
			$args[$name] = array($i, $name, $default, $isVariable);
1458
			$hasVariable |= $isVariable;
1459
		}
1460
1461
		$keywordArgs = array();
1462
		$deferredKeywordArgs = array();
1463
		$remaining = array();
1464
		// assign the keyword args
1465
		foreach ((array) $argValues as $arg) {
1466
			if (!empty($arg[0])) {
1467
				if (!isset($args[$arg[0][1]])) {
1468
					if ($hasVariable) {
1469
						$deferredKeywordArgs[$arg[0][1]] = $arg[1];
1470
					} else {
1471
						$this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
1472
					}
1473
				} elseif ($args[$arg[0][1]][0] < count($remaining)) {
1474
					$this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
1475
				} else {
1476
					$keywordArgs[$arg[0][1]] = $arg[1];
1477
				}
1478
			} elseif (count($keywordArgs)) {
1479
				$this->throwError('Positional arguments must come before keyword arguments.');
1480
			} elseif ($arg[2] == true) {
1481
				$val = $this->reduce($arg[1], true);
1482
				if ($val[0] == 'list') {
1483
					foreach ($val[2] as $name => $item) {
1484
						if (!is_numeric($name)) {
1485
							$keywordArgs[$name] = $item;
1486
						} else {
1487
							$remaining[] = $item;
1488
						}
1489
					}
1490
				} else {
1491
					$remaining[] = $val;
1492
				}
1493
			} else {
1494
				$remaining[] = $arg[1];
1495
			}
1496
		}
1497
1498
		foreach ($args as $arg) {
1499
			list($i, $name, $default, $isVariable) = $arg;
1500
1501
			if ($isVariable) {
1502
				$val = array('list', ',', array());
1503
				for ($count = count($remaining); $i < $count; $i++) {
1504
					$val[2][] = $remaining[$i];
1505
				}
1506
				foreach ($deferredKeywordArgs as $itemName => $item) {
1507
					$val[2][$itemName] = $item;
1508
				}
1509
			} elseif (isset($remaining[$i])) {
1510
				$val = $remaining[$i];
1511
			} elseif (isset($keywordArgs[$name])) {
1512
				$val = $keywordArgs[$name];
1513
			} elseif (!empty($default)) {
1514
				continue;
1515
			} else {
1516
				$this->throwError("Missing argument $name");
1517
			}
1518
1519
			$this->set($name, $this->reduce($val, true), true, $env);
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...
1520
		}
1521
1522
		$storeEnv->store = $env->store;
1523
1524
		foreach ($args as $arg) {
1525
			list($i, $name, $default, $isVariable) = $arg;
1526
1527
			if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) {
1528
				continue;
1529
			}
1530
1531
			$this->set($name, $this->reduce($default, true), true);
1532
		}
1533
	}
1534
1535
	protected function pushEnv($block=null) {
1536
		$env = new stdClass;
1537
		$env->parent = $this->env;
1538
		$env->store = array();
1539
		$env->block = $block;
1540
		$env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
1541
1542
		$this->env = $env;
1543
		return $env;
1544
	}
1545
1546
	protected function normalizeName($name) {
1547
		return str_replace('-', '_', $name);
1548
	}
1549
1550
	protected function getStoreEnv() {
1551
		return isset($this->storeEnv) ? $this->storeEnv : $this->env;
1552
	}
1553
1554
	protected function set($name, $value, $shadow=false, $env = null) {
1555
		$name = $this->normalizeName($name);
1556
1557
		if ($shadow) {
1558
			$this->setRaw($name, $value, $env);
1559
		} else {
1560
			$this->setExisting($name, $value, $env);
1561
		}
1562
	}
1563
1564
	protected function setExisting($name, $value, $env = null) {
1565
		if (!isset($env)) $env = $this->getStoreEnv();
1566
1567
		if (isset($env->store[$name]) || !isset($env->parent)) {
1568
			$env->store[$name] = $value;
1569
		} else {
1570
			$this->setExisting($name, $value, $env->parent);
1571
		}
1572
	}
1573
1574
	protected function setRaw($name, $value, $env = null) {
1575
		if (!isset($env)) $env = $this->getStoreEnv();
1576
1577
		$env->store[$name] = $value;
1578
	}
1579
1580
	public function get($name, $defaultValue = null, $env = null) {
1581
		$name = $this->normalizeName($name);
1582
1583
		if (!isset($env)) $env = $this->getStoreEnv();
1584
		if (!isset($defaultValue)) $defaultValue = self::$defaultValue;
1585
1586
		if (isset($env->store[$name])) {
1587
			return $env->store[$name];
1588
		} elseif (isset($env->parent)) {
1589
			return $this->get($name, $defaultValue, $env->parent);
1590
		}
1591
1592
		return $defaultValue; // found nothing
1593
	}
1594
1595
	protected function injectVariables(array $args)
1596
	{
1597
		if (empty($args)) {
1598
			return;
1599
		}
1600
1601
		$parser = new titanscss_parser(__METHOD__, false);
1602
1603
		foreach ($args as $name => $strValue) {
1604
			if ($name[0] == '$') {
1605
				$name = substr($name, 1);
1606
			}
1607
1608
			$parser->env             = null;
0 ignored issues
show
Bug introduced by
The property env does not seem to exist in titanscss_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...
1609
			$parser->count           = 0;
0 ignored issues
show
Bug introduced by
The property count does not seem to exist in titanscss_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...
1610
			$parser->buffer          = (string) $strValue;
0 ignored issues
show
Bug introduced by
The property buffer does not seem to exist in titanscss_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...
1611
			$parser->inParens        = false;
0 ignored issues
show
Bug introduced by
The property inParens does not seem to exist in titanscss_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...
1612
			$parser->eatWhiteDefault = true;
0 ignored issues
show
Bug introduced by
The property eatWhiteDefault does not seem to exist in titanscss_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...
1613
			$parser->insertComments  = true;
0 ignored issues
show
Bug introduced by
The property insertComments does not seem to exist in titanscss_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...
1614
1615
			if ( ! $parser->valueList($value)) {
1616
				throw new Exception("failed to parse passed in variable $name: $strValue");
1617
			}
1618
1619
			$this->set($name, $value);
1620
		}
1621
	}
1622
1623
	/**
1624
	 * Set variables
1625
	 *
1626
	 * @param array $variables
1627
	 */
1628
	public function setVariables(array $variables)
1629
	{
1630
		$this->registeredVars = array_merge($this->registeredVars, $variables);
1631
	}
1632
1633
	/**
1634
	 * Unset variable
1635
	 *
1636
	 * @param string $name
1637
	 */
1638
	public function unsetVariable($name)
1639
	{
1640
		unset($this->registeredVars[$name]);
1641
	}
1642
1643
	protected function popEnv() {
1644
		$env = $this->env;
1645
		$this->env = $this->env->parent;
1646
		return $env;
1647
	}
1648
1649
	public function getParsedFiles() {
1650
		return $this->parsedFiles;
1651
	}
1652
1653
	public function addImportPath($path) {
1654
		$this->importPaths[] = $path;
1655
	}
1656
1657
	public function setImportPaths($path) {
1658
		$this->importPaths = (array)$path;
1659
	}
1660
1661
	public function setNumberPrecision($numberPrecision) {
1662
		$this->numberPrecision = $numberPrecision;
1663
	}
1664
1665
	public function setFormatter($formatterName) {
1666
		$this->formatter = $formatterName;
1667
	}
1668
1669
	public function registerFunction($name, $func) {
1670
		$this->userFunctions[$this->normalizeName($name)] = $func;
1671
	}
1672
1673
	public function unregisterFunction($name) {
1674
		unset($this->userFunctions[$this->normalizeName($name)]);
1675
	}
1676
1677
	protected function importFile($path, $out) {
1678
		// see if tree is cached
1679
		$realPath = realpath($path);
1680
		if (isset($this->importCache[$realPath])) {
1681
			$tree = $this->importCache[$realPath];
1682
		} else {
1683
			$code = file_get_contents($path);
1684
			$parser = new titanscss_parser($path, false);
1685
			$tree = $parser->parse($code);
1686
			$this->parsedFiles[] = $path;
1687
1688
			$this->importCache[$realPath] = $tree;
1689
		}
1690
1691
		$pi = pathinfo($path);
1692
		array_unshift($this->importPaths, $pi['dirname']);
1693
		$this->compileChildren($tree->children, $out);
1694
		array_shift($this->importPaths);
1695
	}
1696
1697
	// results the file path for an import url if it exists
1698
	public function findImport($url) {
1699
		$urls = array();
1700
1701
		// for "normal" scss imports (ignore vanilla css and external requests)
1702
		if (!preg_match('/\.css|^http:\/\/$/', $url)) {
1703
			// try both normal and the _partial filename
1704
			$urls = array($url, preg_replace('/[^\/]+$/', '_\0', $url));
1705
		}
1706
1707
		foreach ($this->importPaths as $dir) {
1708
			if (is_string($dir)) {
1709
				// check urls for normal import paths
1710
				foreach ($urls as $full) {
1711
					$full = $dir .
1712
						(!empty($dir) && substr($dir, -1) != '/' ? '/' : '') .
1713
						$full;
1714
1715
					if ($this->fileExists($file = $full.'.scss') ||
1716
						$this->fileExists($file = $full))
1717
					{
1718
						return $file;
1719
					}
1720
				}
1721
			} else {
1722
				// check custom callback for import path
1723
				$file = call_user_func($dir,$url,$this);
1724
				if ($file != null) {
1725
					return $file;
1726
				}
1727
			}
1728
		}
1729
1730
		return null;
1731
	}
1732
1733
	protected function fileExists($name) {
1734
		return is_file($name);
1735
	}
1736
1737
	protected function callBuiltin($name, $args, &$returnValue) {
1738
		// try a lib function
1739
		$name = $this->normalizeName($name);
1740
		$libName = 'lib_'.$name;
1741
		$f = array($this, $libName);
1742
		if (is_callable($f)) {
1743
			$prototype = isset(self::$$libName) ? self::$$libName : null;
1744
			$sorted = $this->sortArgs($prototype, $args);
1745
			foreach ($sorted as &$val) {
1746
				$val = $this->reduce($val, true);
1747
			}
1748
			$returnValue = call_user_func($f, $sorted, $this);
1749
		} elseif (isset($this->userFunctions[$name])) {
1750
			// see if we can find a user function
1751
			$fn = $this->userFunctions[$name];
1752
1753
			foreach ($args as &$val) {
1754
				$val = $this->reduce($val[1], true);
1755
			}
1756
1757
			$returnValue = call_user_func($fn, $args, $this);
1758
		}
1759
1760
		if (isset($returnValue)) {
1761
			// coerce a php value into a scss one
1762
			if (is_numeric($returnValue)) {
1763
				$returnValue = array('number', $returnValue, '');
1764
			} elseif (is_bool($returnValue)) {
1765
				$returnValue = $returnValue ? self::$true : self::$false;
1766
			} elseif (!is_array($returnValue)) {
1767
				$returnValue = array('keyword', $returnValue);
1768
			}
1769
1770
			return true;
1771
		}
1772
1773
		return false;
1774
	}
1775
1776
	// sorts any keyword arguments
1777
	// TODO: merge with apply arguments
1778
	protected function sortArgs($prototype, $args) {
1779
		$keyArgs = array();
1780
		$posArgs = array();
1781
1782
		foreach ($args as $arg) {
1783
			list($key, $value) = $arg;
1784
			$key = $key[1];
1785
			if (empty($key)) {
1786
				$posArgs[] = $value;
1787
			} else {
1788
				$keyArgs[$key] = $value;
1789
			}
1790
		}
1791
1792
		if (!isset($prototype)) return $posArgs;
1793
1794
		$finalArgs = array();
1795
		foreach ($prototype as $i => $names) {
1796
			if (isset($posArgs[$i])) {
1797
				$finalArgs[] = $posArgs[$i];
1798
				continue;
1799
			}
1800
1801
			$set = false;
1802
			foreach ((array)$names as $name) {
1803
				if (isset($keyArgs[$name])) {
1804
					$finalArgs[] = $keyArgs[$name];
1805
					$set = true;
1806
					break;
1807
				}
1808
			}
1809
1810
			if (!$set) {
1811
				$finalArgs[] = null;
1812
			}
1813
		}
1814
1815
		return $finalArgs;
1816
	}
1817
1818
	protected function coerceForExpression($value) {
1819
		if ($color = $this->coerceColor($value)) {
1820
			return $color;
1821
		}
1822
1823
		return $value;
1824
	}
1825
1826
	protected function coerceColor($value) {
1827
		switch ($value[0]) {
1828
		case 'color': return $value;
1829
		case 'keyword':
1830
			$name = $value[1];
1831
			if (isset(self::$cssColors[$name])) {
1832
				$rgba = explode(',', self::$cssColors[$name]);
1833
				return isset($rgba[3])
1834
					? array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3])
1835
					: array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]);
1836
			}
1837
			return null;
1838
		}
1839
1840
		return null;
1841
	}
1842
1843
	protected function coerceString($value) {
1844
		switch ($value[0]) {
1845
		case 'string':
1846
			return $value;
1847
		case 'keyword':
1848
			return array('string', '', array($value[1]));
1849
		}
1850
		return null;
1851
	}
1852
1853
	public function assertList($value) {
1854
		if ($value[0] != 'list')
1855
			$this->throwError('expecting list');
1856
		return $value;
1857
	}
1858
1859
	public function assertColor($value) {
1860
		if ($color = $this->coerceColor($value)) return $color;
1861
		$this->throwError('expecting color');
1862
	}
1863
1864
	public function assertNumber($value) {
1865
		if ($value[0] != 'number')
1866
			$this->throwError('expecting number');
1867
		return $value[1];
1868
	}
1869
1870
	protected function coercePercent($value) {
1871
		if ($value[0] == 'number') {
1872
			if ($value[2] == '%') {
1873
				return $value[1] / 100;
1874
			}
1875
			return $value[1];
1876
		}
1877
		return 0;
1878
	}
1879
1880
	// make sure a color's components don't go out of bounds
1881
	protected function fixColor($c) {
1882
		foreach (range(1, 3) as $i) {
1883
			if ($c[$i] < 0) $c[$i] = 0;
1884
			if ($c[$i] > 255) $c[$i] = 255;
1885
		}
1886
1887
		return $c;
1888
	}
1889
1890
	public function toHSL($red, $green, $blue) {
1891
		$min = min($red, $green, $blue);
1892
		$max = max($red, $green, $blue);
1893
1894
		$l = $min + $max;
1895
1896
		if ($min == $max) {
1897
			$s = $h = 0;
1898
		} else {
1899
			$d = $max - $min;
1900
1901
			if ($l < 255)
1902
				$s = $d / $l;
1903
			else
1904
				$s = $d / (510 - $l);
1905
1906
			if ($red == $max)
1907
				$h = 60 * ($green - $blue) / $d;
1908
			elseif ($green == $max)
1909
				$h = 60 * ($blue - $red) / $d + 120;
1910
			elseif ($blue == $max)
1911
				$h = 60 * ($red - $green) / $d + 240;
1912
		}
1913
1914
		return array('hsl', fmod($h, 360), $s * 100, $l / 5.1);
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...
1915
	}
1916
1917
	public function hueToRGB($m1, $m2, $h) {
1918
		if ($h < 0)
1919
			$h += 1;
1920
		elseif ($h > 1)
1921
			$h -= 1;
1922
1923
		if ($h * 6 < 1)
1924
			return $m1 + ($m2 - $m1) * $h * 6;
1925
1926
		if ($h * 2 < 1)
1927
			return $m2;
1928
1929
		if ($h * 3 < 2)
1930
			return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
1931
1932
		return $m1;
1933
	}
1934
1935
	// H from 0 to 360, S and L from 0 to 100
1936
	public function toRGB($hue, $saturation, $lightness) {
1937
		if ($hue < 0) {
1938
			$hue += 360;
1939
		}
1940
1941
		$h = $hue / 360;
1942
		$s = min(100, max(0, $saturation)) / 100;
1943
		$l = min(100, max(0, $lightness)) / 100;
1944
1945
		$m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
1946
		$m1 = $l * 2 - $m2;
1947
1948
		$r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
1949
		$g = $this->hueToRGB($m1, $m2, $h) * 255;
1950
		$b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
1951
1952
		$out = array('color', $r, $g, $b);
1953
		return $out;
1954
	}
1955
1956
	// Built in functions
1957
1958
	protected static $lib_if = array('condition', 'if-true', 'if-false');
1959
	protected function lib_if($args) {
1960
		list($cond,$t, $f) = $args;
1961
		if (!$this->isTruthy($cond)) return $f;
1962
		return $t;
1963
	}
1964
1965
	protected static $lib_index = array('list', 'value');
1966
	protected function lib_index($args) {
1967
		list($list, $value) = $args;
1968
		$list = $this->assertList($list);
1969
1970
		$values = array();
1971
		foreach ($list[2] as $item) {
1972
			$values[] = $this->normalizeValue($item);
1973
		}
1974
		$key = array_search($this->normalizeValue($value), $values);
1975
1976
		return false == $key ? false : $key + 1;
1977
	}
1978
1979
	protected static $lib_rgb = array('red', 'green', 'blue');
1980
	protected function lib_rgb($args) {
1981
		list($r,$g,$b) = $args;
1982
		return array('color', $r[1], $g[1], $b[1]);
1983
	}
1984
1985
	protected static $lib_rgba = array(
1986
		array('red', 'color'),
1987
		'green', 'blue', 'alpha');
1988
	protected function lib_rgba($args) {
1989
		if ($color = $this->coerceColor($args[0])) {
1990
			$num = !isset($args[1]) ? $args[3] : $args[1];
1991
			$alpha = $this->assertNumber($num);
1992
			$color[4] = $alpha;
1993
			return $color;
1994
		}
1995
1996
		list($r,$g,$b, $a) = $args;
1997
		return array('color', $r[1], $g[1], $b[1], $a[1]);
1998
	}
1999
2000
	// helper function for adjust_color, change_color, and scale_color
2001
	protected function alter_color($args, $fn) {
2002
		$color = $this->assertColor($args[0]);
2003
2004
		foreach (array(1,2,3,7) as $i) {
2005
			if (isset($args[$i])) {
2006
				$val = $this->assertNumber($args[$i]);
2007
				$ii = $i == 7 ? 4 : $i; // alpha
2008
				$color[$ii] =
2009
					$this->$fn(isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
2010
			}
2011
		}
2012
2013
		if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
2014
			$hsl = $this->toHSL($color[1], $color[2], $color[3]);
2015
			foreach (array(4,5,6) as $i) {
2016
				if (isset($args[$i])) {
2017
					$val = $this->assertNumber($args[$i]);
2018
					$hsl[$i - 3] = $this->$fn($hsl[$i - 3], $val, $i);
2019
				}
2020
			}
2021
2022
			$rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
2023
			if (isset($color[4])) $rgb[4] = $color[4];
2024
			$color = $rgb;
2025
		}
2026
2027
		return $color;
2028
	}
2029
2030
	protected static $lib_adjust_color = array(
2031
		'color', 'red', 'green', 'blue',
2032
		'hue', 'saturation', 'lightness', 'alpha'
2033
	);
2034
	protected function adjust_color_helper($base, $alter, $i) {
0 ignored issues
show
Unused Code introduced by
The parameter $i is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2035
		return $base += $alter;
2036
	}
2037
	protected function lib_adjust_color($args) {
2038
		return $this->alter_color($args, 'adjust_color_helper');
2039
	}
2040
2041
	protected static $lib_change_color = array(
2042
		'color', 'red', 'green', 'blue',
2043
		'hue', 'saturation', 'lightness', 'alpha'
2044
	);
2045
	protected function change_color_helper($base, $alter, $i) {
0 ignored issues
show
Unused Code introduced by
The parameter $base is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $i is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
2046
		return $alter;
2047
	}
2048
	protected function lib_change_color($args) {
2049
		return $this->alter_color($args, 'change_color_helper');
2050
	}
2051
2052
	protected static $lib_scale_color = array(
2053
		'color', 'red', 'green', 'blue',
2054
		'hue', 'saturation', 'lightness', 'alpha'
2055
	);
2056
	protected function scale_color_helper($base, $scale, $i) {
2057
		// 1,2,3 - rgb
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2058
		// 4, 5, 6 - hsl
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2059
		// 7 - a
2060
		switch ($i) {
2061
		case 1:
2062
		case 2:
2063
		case 3:
2064
			$max = 255; break;
2065
		case 4:
2066
			$max = 360; break;
2067
		case 7:
2068
			$max = 1; break;
2069
		default:
2070
			$max = 100;
2071
		}
2072
2073
		$scale = $scale / 100;
2074
		if ($scale < 0) {
2075
			return $base * $scale + $base;
2076
		} else {
2077
			return ($max - $base) * $scale + $base;
2078
		}
2079
	}
2080
	protected function lib_scale_color($args) {
2081
		return $this->alter_color($args, 'scale_color_helper');
2082
	}
2083
2084
	protected static $lib_ie_hex_str = array('color');
2085
	protected function lib_ie_hex_str($args) {
2086
		$color = $this->coerceColor($args[0]);
2087
		$color[4] = isset($color[4]) ? round(255*$color[4]) : 255;
2088
2089
		return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
2090
	}
2091
2092
	protected static $lib_red = array('color');
2093
	protected function lib_red($args) {
2094
		$color = $this->coerceColor($args[0]);
2095
		return $color[1];
2096
	}
2097
2098
	protected static $lib_green = array('color');
2099
	protected function lib_green($args) {
2100
		$color = $this->coerceColor($args[0]);
2101
		return $color[2];
2102
	}
2103
2104
	protected static $lib_blue = array('color');
2105
	protected function lib_blue($args) {
2106
		$color = $this->coerceColor($args[0]);
2107
		return $color[3];
2108
	}
2109
2110
	protected static $lib_alpha = array('color');
2111
	protected function lib_alpha($args) {
2112
		if ($color = $this->coerceColor($args[0])) {
2113
			return isset($color[4]) ? $color[4] : 1;
2114
		}
2115
2116
		// this might be the IE function, so return value unchanged
2117
		return null;
2118
	}
2119
2120
	protected static $lib_opacity = array('color');
2121
	protected function lib_opacity($args) {
2122
		$value = $args[0];
2123
		if ($value[0] == 'number') return null;
2124
		return $this->lib_alpha($args);
2125
	}
2126
2127
	// mix two colors
2128
	protected static $lib_mix = array('color-1', 'color-2', 'weight');
2129
	protected function lib_mix($args) {
2130
		list($first, $second, $weight) = $args;
2131
		$first = $this->assertColor($first);
2132
		$second = $this->assertColor($second);
2133
2134
		if (!isset($weight)) {
2135
			$weight = 0.5;
2136
		} else {
2137
			$weight = $this->coercePercent($weight);
2138
		}
2139
2140
		$firstAlpha = isset($first[4]) ? $first[4] : 1;
2141
		$secondAlpha = isset($second[4]) ? $second[4] : 1;
2142
2143
		$w = $weight * 2 - 1;
2144
		$a = $firstAlpha - $secondAlpha;
2145
2146
		$w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
2147
		$w2 = 1.0 - $w1;
2148
2149
		$new = array('color',
2150
			$w1 * $first[1] + $w2 * $second[1],
2151
			$w1 * $first[2] + $w2 * $second[2],
2152
			$w1 * $first[3] + $w2 * $second[3],
2153
		);
2154
2155
		if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
2156
			$new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1);
2157
		}
2158
2159
		return $this->fixColor($new);
2160
	}
2161
2162
	protected static $lib_hsl = array('hue', 'saturation', 'lightness');
2163
	protected function lib_hsl($args) {
2164
		list($h, $s, $l) = $args;
2165
		return $this->toRGB($h[1], $s[1], $l[1]);
2166
	}
2167
2168
	protected static $lib_hsla = array('hue', 'saturation',
2169
		'lightness', 'alpha');
2170
	protected function lib_hsla($args) {
2171
		list($h, $s, $l, $a) = $args;
2172
		$color = $this->toRGB($h[1], $s[1], $l[1]);
2173
		$color[4] = $a[1];
2174
		return $color;
2175
	}
2176
2177
	protected static $lib_hue = array('color');
2178
	protected function lib_hue($args) {
2179
		$color = $this->assertColor($args[0]);
2180
		$hsl = $this->toHSL($color[1], $color[2], $color[3]);
2181
		return array('number', $hsl[1], 'deg');
2182
	}
2183
2184
	protected static $lib_saturation = array('color');
2185
	protected function lib_saturation($args) {
2186
		$color = $this->assertColor($args[0]);
2187
		$hsl = $this->toHSL($color[1], $color[2], $color[3]);
2188
		return array('number', $hsl[2], '%');
2189
	}
2190
2191
	protected static $lib_lightness = array('color');
2192
	protected function lib_lightness($args) {
2193
		$color = $this->assertColor($args[0]);
2194
		$hsl = $this->toHSL($color[1], $color[2], $color[3]);
2195
		return array('number', $hsl[3], '%');
2196
	}
2197
2198
	protected function adjustHsl($color, $idx, $amount) {
2199
		$hsl = $this->toHSL($color[1], $color[2], $color[3]);
2200
		$hsl[$idx] += $amount;
2201
		$out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
2202
		if (isset($color[4])) $out[4] = $color[4];
2203
		return $out;
2204
	}
2205
2206
	protected static $lib_adjust_hue = array('color', 'degrees');
2207
	protected function lib_adjust_hue($args) {
2208
		$color = $this->assertColor($args[0]);
2209
		$degrees = $this->assertNumber($args[1]);
2210
		return $this->adjustHsl($color, 1, $degrees);
2211
	}
2212
2213
	protected static $lib_lighten = array('color', 'amount');
2214
	protected function lib_lighten($args) {
2215
		$color = $this->assertColor($args[0]);
2216
		$amount = 100*$this->coercePercent($args[1]);
2217
		return $this->adjustHsl($color, 3, $amount);
2218
	}
2219
2220
	protected static $lib_darken = array('color', 'amount');
2221
	protected function lib_darken($args) {
2222
		$color = $this->assertColor($args[0]);
2223
		$amount = 100*$this->coercePercent($args[1]);
2224
		return $this->adjustHsl($color, 3, -$amount);
2225
	}
2226
2227
	protected static $lib_saturate = array('color', 'amount');
2228
	protected function lib_saturate($args) {
2229
		$value = $args[0];
2230
		if ($value[0] == 'number') return null;
2231
		$color = $this->assertColor($value);
2232
		$amount = 100*$this->coercePercent($args[1]);
2233
		return $this->adjustHsl($color, 2, $amount);
2234
	}
2235
2236
	protected static $lib_desaturate = array('color', 'amount');
2237
	protected function lib_desaturate($args) {
2238
		$color = $this->assertColor($args[0]);
2239
		$amount = 100*$this->coercePercent($args[1]);
2240
		return $this->adjustHsl($color, 2, -$amount);
2241
	}
2242
2243
	protected static $lib_grayscale = array('color');
2244
	protected function lib_grayscale($args) {
2245
		$value = $args[0];
2246
		if ($value[0] == 'number') return null;
2247
		return $this->adjustHsl($this->assertColor($value), 2, -100);
2248
	}
2249
2250
	protected static $lib_complement = array('color');
2251
	protected function lib_complement($args) {
2252
		return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
2253
	}
2254
2255
	protected static $lib_invert = array('color');
2256
	protected function lib_invert($args) {
2257
		$value = $args[0];
2258
		if ($value[0] == 'number') return null;
2259
		$color = $this->assertColor($value);
2260
		$color[1] = 255 - $color[1];
2261
		$color[2] = 255 - $color[2];
2262
		$color[3] = 255 - $color[3];
2263
		return $color;
2264
	}
2265
2266
	// increases opacity by amount
2267
	protected static $lib_opacify = array('color', 'amount');
2268
	protected function lib_opacify($args) {
2269
		$color = $this->assertColor($args[0]);
2270
		$amount = $this->coercePercent($args[1]);
2271
2272
		$color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
2273
		$color[4] = min(1, max(0, $color[4]));
2274
		return $color;
2275
	}
2276
2277
	protected static $lib_fade_in = array('color', 'amount');
2278
	protected function lib_fade_in($args) {
2279
		return $this->lib_opacify($args);
2280
	}
2281
2282
	// decreases opacity by amount
2283
	protected static $lib_transparentize = array('color', 'amount');
2284
	protected function lib_transparentize($args) {
2285
		$color = $this->assertColor($args[0]);
2286
		$amount = $this->coercePercent($args[1]);
2287
2288
		$color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
2289
		$color[4] = min(1, max(0, $color[4]));
2290
		return $color;
2291
	}
2292
2293
	protected static $lib_fade_out = array('color', 'amount');
2294
	protected function lib_fade_out($args) {
2295
		return $this->lib_transparentize($args);
2296
	}
2297
2298
	protected static $lib_unquote = array('string');
2299
	protected function lib_unquote($args) {
2300
		$str = $args[0];
2301
		if ($str[0] == 'string') $str[1] = '';
2302
		return $str;
2303
	}
2304
2305
	protected static $lib_quote = array('string');
2306
	protected function lib_quote($args) {
2307
		$value = $args[0];
2308
		if ($value[0] == 'string' && !empty($value[1]))
2309
			return $value;
2310
		return array('string', '"', array($value));
2311
	}
2312
2313
	protected static $lib_percentage = array('value');
2314
	protected function lib_percentage($args) {
2315
		return array('number',
2316
			$this->coercePercent($args[0]) * 100,
2317
			'%');
2318
	}
2319
2320
	protected static $lib_round = array('value');
2321
	protected function lib_round($args) {
2322
		$num = $args[0];
2323
		$num[1] = round($num[1]);
2324
		return $num;
2325
	}
2326
2327
	protected static $lib_floor = array('value');
2328
	protected function lib_floor($args) {
2329
		$num = $args[0];
2330
		$num[1] = floor($num[1]);
2331
		return $num;
2332
	}
2333
2334
	protected static $lib_ceil = array('value');
2335
	protected function lib_ceil($args) {
2336
		$num = $args[0];
2337
		$num[1] = ceil($num[1]);
2338
		return $num;
2339
	}
2340
2341
	protected static $lib_abs = array('value');
2342
	protected function lib_abs($args) {
2343
		$num = $args[0];
2344
		$num[1] = abs($num[1]);
2345
		return $num;
2346
	}
2347
2348
	protected function lib_min($args) {
2349
		$numbers = $this->getNormalizedNumbers($args);
2350
		$min = null;
2351
		foreach ($numbers as $key => $number) {
2352
			if (null == $min || $number[1] <= $min[1]) {
2353
				$min = array($key, $number[1]);
2354
			}
2355
		}
2356
2357
		return $args[$min[0]];
2358
	}
2359
2360
	protected function lib_max($args) {
2361
		$numbers = $this->getNormalizedNumbers($args);
2362
		$max = null;
2363
		foreach ($numbers as $key => $number) {
2364
			if (null == $max || $number[1] >= $max[1]) {
2365
				$max = array($key, $number[1]);
2366
			}
2367
		}
2368
2369
		return $args[$max[0]];
2370
	}
2371
2372
	protected function getNormalizedNumbers($args) {
2373
		$unit = null;
2374
		$originalUnit = null;
2375
		$numbers = array();
2376
		foreach ($args as $key => $item) {
2377
			if ('number' != $item[0]) {
2378
				$this->throwError('%s is not a number', $item[0]);
2379
			}
2380
			$number = $this->normalizeNumber($item);
2381
2382
			if (null == $unit) {
2383
				$unit = $number[2];
2384
				$originalUnit = $item[2];
2385
			} elseif ($unit != $number[2]) {
2386
				$this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item[2]);
2387
			}
2388
2389
			$numbers[$key] = $number;
2390
		}
2391
2392
		return $numbers;
2393
	}
2394
2395
	protected static $lib_length = array('list');
2396
	protected function lib_length($args) {
2397
		$list = $this->coerceList($args[0]);
2398
		return count($list[2]);
2399
	}
2400
2401
	protected static $lib_nth = array('list', 'n');
2402
	protected function lib_nth($args) {
2403
		$list = $this->coerceList($args[0]);
2404
		$n = $this->assertNumber($args[1]) - 1;
2405
		return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue;
2406
	}
2407
2408
	protected function listSeparatorForJoin($list1, $sep) {
2409
		if (!isset($sep)) return $list1[1];
2410
		switch ($this->compileValue($sep)) {
2411
		case 'comma':
2412
			return ',';
2413
		case 'space':
2414
			return '';
2415
		default:
2416
			return $list1[1];
2417
		}
2418
	}
2419
2420
	protected static $lib_join = array('list1', 'list2', 'separator');
2421
	protected function lib_join($args) {
2422
		list($list1, $list2, $sep) = $args;
2423
		$list1 = $this->coerceList($list1, ' ');
2424
		$list2 = $this->coerceList($list2, ' ');
2425
		$sep = $this->listSeparatorForJoin($list1, $sep);
2426
		return array('list', $sep, array_merge($list1[2], $list2[2]));
2427
	}
2428
2429
	protected static $lib_append = array('list', 'val', 'separator');
2430
	protected function lib_append($args) {
2431
		list($list1, $value, $sep) = $args;
2432
		$list1 = $this->coerceList($list1, ' ');
2433
		$sep = $this->listSeparatorForJoin($list1, $sep);
2434
		return array('list', $sep, array_merge($list1[2], array($value)));
2435
	}
2436
2437
	protected function lib_zip($args) {
2438
		foreach ($args as $arg) {
2439
			$this->assertList($arg);
2440
		}
2441
2442
		$lists = array();
2443
		$firstList = array_shift($args);
2444
		foreach ($firstList[2] as $key => $item) {
2445
			$list = array('list', '', array($item));
2446
			foreach ($args as $arg) {
2447
				if (isset($arg[2][$key])) {
2448
					$list[2][] = $arg[2][$key];
2449
				} else {
2450
					break 2;
2451
				}
2452
			}
2453
			$lists[] = $list;
2454
		}
2455
2456
		return array('list', ',', $lists);
2457
	}
2458
2459
	protected static $lib_type_of = array('value');
2460
	protected function lib_type_of($args) {
2461
		$value = $args[0];
2462
		switch ($value[0]) {
2463
		case 'keyword':
2464
			if ($value == self::$true || $value == self::$false) {
2465
				return 'bool';
2466
			}
2467
2468
			if ($this->coerceColor($value)) {
2469
				return 'color';
2470
			}
2471
2472
			return 'string';
2473
		default:
2474
			return $value[0];
2475
		}
2476
	}
2477
2478
	protected static $lib_unit = array('number');
2479
	protected function lib_unit($args) {
2480
		$num = $args[0];
2481
		if ($num[0] == 'number') {
2482
			return array('string', '"', array($num[2]));
2483
		}
2484
		return '';
2485
	}
2486
2487
	protected static $lib_unitless = array('number');
2488
	protected function lib_unitless($args) {
2489
		$value = $args[0];
2490
		return $value[0] == 'number' && empty($value[2]);
2491
	}
2492
2493
	protected static $lib_comparable = array('number-1', 'number-2');
2494
	protected function lib_comparable($args) {
2495
		list($number1, $number2) = $args;
2496
		if (!isset($number1[0]) || $number1[0] != 'number' || !isset($number2[0]) || $number2[0] != 'number') {
2497
			$this->throwError('Invalid argument(s) for "comparable"');
2498
		}
2499
2500
		$number1 = $this->normalizeNumber($number1);
2501
		$number2 = $this->normalizeNumber($number2);
2502
2503
		return $number1[2] == $number2[2] || $number1[2] == '' || $number2[2] == '';
2504
	}
2505
2506
	/**
2507
	 * Workaround IE7's content counter bug.
2508
	 *
2509
	 * @param array $args
2510
	 */
2511
	protected function lib_counter($args) {
2512
		$list = array_map(array($this, 'compileValue'), $args);
2513
		return array('string', '', array('counter(' . implode(',', $list) . ')'));
2514
	}
2515
2516
	public function throwError($msg = null) {
2517
		if (func_num_args() > 1) {
2518
			$msg = call_user_func_array('sprintf', func_get_args());
2519
		}
2520
2521
		if ($this->sourcePos >= 0 && isset($this->sourceParser)) {
2522
			$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...
2523
		}
2524
2525
		throw new Exception($msg);
2526
	}
2527
2528
	/**
2529
	 * CSS Colors
2530
	 *
2531
	 * @see http://www.w3.org/TR/css3-color
2532
	 */
2533
	static protected $cssColors = array(
2534
		'aliceblue' => '240,248,255',
2535
		'antiquewhite' => '250,235,215',
2536
		'aqua' => '0,255,255',
2537
		'aquamarine' => '127,255,212',
2538
		'azure' => '240,255,255',
2539
		'beige' => '245,245,220',
2540
		'bisque' => '255,228,196',
2541
		'black' => '0,0,0',
2542
		'blanchedalmond' => '255,235,205',
2543
		'blue' => '0,0,255',
2544
		'blueviolet' => '138,43,226',
2545
		'brown' => '165,42,42',
2546
		'burlywood' => '222,184,135',
2547
		'cadetblue' => '95,158,160',
2548
		'chartreuse' => '127,255,0',
2549
		'chocolate' => '210,105,30',
2550
		'coral' => '255,127,80',
2551
		'cornflowerblue' => '100,149,237',
2552
		'cornsilk' => '255,248,220',
2553
		'crimson' => '220,20,60',
2554
		'cyan' => '0,255,255',
2555
		'darkblue' => '0,0,139',
2556
		'darkcyan' => '0,139,139',
2557
		'darkgoldenrod' => '184,134,11',
2558
		'darkgray' => '169,169,169',
2559
		'darkgreen' => '0,100,0',
2560
		'darkgrey' => '169,169,169',
2561
		'darkkhaki' => '189,183,107',
2562
		'darkmagenta' => '139,0,139',
2563
		'darkolivegreen' => '85,107,47',
2564
		'darkorange' => '255,140,0',
2565
		'darkorchid' => '153,50,204',
2566
		'darkred' => '139,0,0',
2567
		'darksalmon' => '233,150,122',
2568
		'darkseagreen' => '143,188,143',
2569
		'darkslateblue' => '72,61,139',
2570
		'darkslategray' => '47,79,79',
2571
		'darkslategrey' => '47,79,79',
2572
		'darkturquoise' => '0,206,209',
2573
		'darkviolet' => '148,0,211',
2574
		'deeppink' => '255,20,147',
2575
		'deepskyblue' => '0,191,255',
2576
		'dimgray' => '105,105,105',
2577
		'dimgrey' => '105,105,105',
2578
		'dodgerblue' => '30,144,255',
2579
		'firebrick' => '178,34,34',
2580
		'floralwhite' => '255,250,240',
2581
		'forestgreen' => '34,139,34',
2582
		'fuchsia' => '255,0,255',
2583
		'gainsboro' => '220,220,220',
2584
		'ghostwhite' => '248,248,255',
2585
		'gold' => '255,215,0',
2586
		'goldenrod' => '218,165,32',
2587
		'gray' => '128,128,128',
2588
		'green' => '0,128,0',
2589
		'greenyellow' => '173,255,47',
2590
		'grey' => '128,128,128',
2591
		'honeydew' => '240,255,240',
2592
		'hotpink' => '255,105,180',
2593
		'indianred' => '205,92,92',
2594
		'indigo' => '75,0,130',
2595
		'ivory' => '255,255,240',
2596
		'khaki' => '240,230,140',
2597
		'lavender' => '230,230,250',
2598
		'lavenderblush' => '255,240,245',
2599
		'lawngreen' => '124,252,0',
2600
		'lemonchiffon' => '255,250,205',
2601
		'lightblue' => '173,216,230',
2602
		'lightcoral' => '240,128,128',
2603
		'lightcyan' => '224,255,255',
2604
		'lightgoldenrodyellow' => '250,250,210',
2605
		'lightgray' => '211,211,211',
2606
		'lightgreen' => '144,238,144',
2607
		'lightgrey' => '211,211,211',
2608
		'lightpink' => '255,182,193',
2609
		'lightsalmon' => '255,160,122',
2610
		'lightseagreen' => '32,178,170',
2611
		'lightskyblue' => '135,206,250',
2612
		'lightslategray' => '119,136,153',
2613
		'lightslategrey' => '119,136,153',
2614
		'lightsteelblue' => '176,196,222',
2615
		'lightyellow' => '255,255,224',
2616
		'lime' => '0,255,0',
2617
		'limegreen' => '50,205,50',
2618
		'linen' => '250,240,230',
2619
		'magenta' => '255,0,255',
2620
		'maroon' => '128,0,0',
2621
		'mediumaquamarine' => '102,205,170',
2622
		'mediumblue' => '0,0,205',
2623
		'mediumorchid' => '186,85,211',
2624
		'mediumpurple' => '147,112,219',
2625
		'mediumseagreen' => '60,179,113',
2626
		'mediumslateblue' => '123,104,238',
2627
		'mediumspringgreen' => '0,250,154',
2628
		'mediumturquoise' => '72,209,204',
2629
		'mediumvioletred' => '199,21,133',
2630
		'midnightblue' => '25,25,112',
2631
		'mintcream' => '245,255,250',
2632
		'mistyrose' => '255,228,225',
2633
		'moccasin' => '255,228,181',
2634
		'navajowhite' => '255,222,173',
2635
		'navy' => '0,0,128',
2636
		'oldlace' => '253,245,230',
2637
		'olive' => '128,128,0',
2638
		'olivedrab' => '107,142,35',
2639
		'orange' => '255,165,0',
2640
		'orangered' => '255,69,0',
2641
		'orchid' => '218,112,214',
2642
		'palegoldenrod' => '238,232,170',
2643
		'palegreen' => '152,251,152',
2644
		'paleturquoise' => '175,238,238',
2645
		'palevioletred' => '219,112,147',
2646
		'papayawhip' => '255,239,213',
2647
		'peachpuff' => '255,218,185',
2648
		'peru' => '205,133,63',
2649
		'pink' => '255,192,203',
2650
		'plum' => '221,160,221',
2651
		'powderblue' => '176,224,230',
2652
		'purple' => '128,0,128',
2653
		'red' => '255,0,0',
2654
		'rosybrown' => '188,143,143',
2655
		'royalblue' => '65,105,225',
2656
		'saddlebrown' => '139,69,19',
2657
		'salmon' => '250,128,114',
2658
		'sandybrown' => '244,164,96',
2659
		'seagreen' => '46,139,87',
2660
		'seashell' => '255,245,238',
2661
		'sienna' => '160,82,45',
2662
		'silver' => '192,192,192',
2663
		'skyblue' => '135,206,235',
2664
		'slateblue' => '106,90,205',
2665
		'slategray' => '112,128,144',
2666
		'slategrey' => '112,128,144',
2667
		'snow' => '255,250,250',
2668
		'springgreen' => '0,255,127',
2669
		'steelblue' => '70,130,180',
2670
		'tan' => '210,180,140',
2671
		'teal' => '0,128,128',
2672
		'thistle' => '216,191,216',
2673
		'tomato' => '255,99,71',
2674
		'transparent' => '0,0,0,0',
2675
		'turquoise' => '64,224,208',
2676
		'violet' => '238,130,238',
2677
		'wheat' => '245,222,179',
2678
		'white' => '255,255,255',
2679
		'whitesmoke' => '245,245,245',
2680
		'yellow' => '255,255,0',
2681
		'yellowgreen' => '154,205,50'
2682
	);
2683
}
2684
2685
/**
2686
 * SCSS parser
2687
 *
2688
 * @author Leaf Corcoran <[email protected]>
2689
 */
2690
class titanscss_parser {
2691
	static protected $precedence = array(
2692
		'or' => 0,
2693
		'and' => 1,
2694
2695
		'==' => 2,
2696
		'!=' => 2,
2697
		'<=' => 2,
2698
		'>=' => 2,
2699
		'=' => 2,
2700
		'<' => 3,
2701
		'>' => 2,
2702
2703
		'+' => 3,
2704
		'-' => 3,
2705
		'*' => 4,
2706
		'/' => 4,
2707
		'%' => 4,
2708
	);
2709
2710
	static protected $operators = array('+', '-', '*', '/', '%',
2711
		'==', '!=', '<=', '>=', '<', '>', 'and', 'or');
2712
2713
	static protected $operatorStr;
2714
	static protected $whitePattern;
2715
	static protected $commentMulti;
2716
2717
	static protected $commentSingle = '//';
2718
	static protected $commentMultiLeft = '/*';
2719
	static protected $commentMultiRight = '*/';
2720
2721
	/**
2722
	 * Constructor
2723
	 *
2724
	 * @param string  $sourceName
2725
	 * @param boolean $rootParser
2726
	 */
2727
	public function __construct($sourceName = null, $rootParser = true) {
2728
		$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...
2729
		$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...
2730
2731
		if (empty(self::$operatorStr)) {
2732
			self::$operatorStr = $this->makeOperatorStr(self::$operators);
2733
2734
			$commentSingle = $this->preg_quote(self::$commentSingle);
2735
			$commentMultiLeft = $this->preg_quote(self::$commentMultiLeft);
2736
			$commentMultiRight = $this->preg_quote(self::$commentMultiRight);
2737
			self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
2738
			self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
2739
		}
2740
	}
2741
2742
	static protected function makeOperatorStr($operators) {
0 ignored issues
show
Coding Style introduced by
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
2743
		return '('.implode('|', array_map(array('titanscss_parser','preg_quote'),
2744
			$operators)).')';
2745
	}
2746
2747
	/**
2748
	 * Parser buffer
2749
	 *
2750
	 * @param string $buffer;
0 ignored issues
show
Documentation introduced by
There is no parameter named $buffer;. Did you maybe mean $buffer?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
2751
	 *
2752
	 * @return \StdClass
2753
	 */
2754
	public function parse($buffer)
2755
	{
2756
		$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...
2757
		$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...
2758
		$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...
2759
		$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...
2760
		$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...
2761
2762
		$this->pushBlock(null); // root block
2763
2764
		$this->whitespace();
2765
		$this->pushBlock(null);
2766
		$this->popBlock();
2767
2768
		while (false != $this->parseChunk())
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison !== instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
2769
			;
2770
2771
		if ($this->count != strlen($this->buffer)) {
2772
			$this->throwParseError();
2773
		}
2774
2775
		if (!empty($this->env->parent)) {
2776
			$this->throwParseError('unclosed block');
2777
		}
2778
2779
		$this->env->isRoot    = true;
2780
2781
		return $this->env;
2782
	}
2783
2784
	/**
2785
	 * Parse a single chunk off the head of the buffer and append it to the
2786
	 * current parse environment.
2787
	 *
2788
	 * Returns false when the buffer is empty, or when there is an error.
2789
	 *
2790
	 * This function is called repeatedly until the entire document is
2791
	 * parsed.
2792
	 *
2793
	 * This parser is most similar to a recursive descent parser. Single
2794
	 * functions represent discrete grammatical rules for the language, and
2795
	 * they are able to capture the text that represents those rules.
2796
	 *
2797
	 * Consider the function scssc::keyword(). (All parse functions are
2798
	 * structured the same.)
2799
	 *
2800
	 * The function takes a single reference argument. When calling the
2801
	 * function it will attempt to match a keyword on the head of the buffer.
2802
	 * If it is successful, it will place the keyword in the referenced
2803
	 * argument, advance the position in the buffer, and return true. If it
2804
	 * fails then it won't advance the buffer and it will return false.
2805
	 *
2806
	 * All of these parse functions are powered by scssc::match(), which behaves
2807
	 * the same way, but takes a literal regular expression. Sometimes it is
2808
	 * more convenient to use match instead of creating a new function.
2809
	 *
2810
	 * Because of the format of the functions, to parse an entire string of
2811
	 * grammatical rules, you can chain them together using &&.
2812
	 *
2813
	 * But, if some of the rules in the chain succeed before one fails, then
2814
	 * the buffer position will be left at an invalid state. In order to
2815
	 * avoid this, scssc::seek() is used to remember and set buffer positions.
2816
	 *
2817
	 * Before parsing a chain, use $s = $this->seek() to remember the current
2818
	 * position into $s. Then if a chain fails, use $this->seek($s) to
2819
	 * go back where we started.
2820
	 *
2821
	 * @return boolean
2822
	 */
2823
	protected function parseChunk() {
2824
		$s = $this->seek();
2825
2826
		// the directives
2827
		if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == '@') {
2828
			if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) {
2829
				$media = $this->pushSpecialBlock('media');
2830
				$media->queryList = $mediaQueryList[2];
2831
				return true;
2832
			} else {
2833
				$this->seek($s);
2834
			}
2835
2836
			if ($this->literal('@mixin') &&
2837
				$this->keyword($mixinName) &&
2838
				($this->argumentDef($args) || true) &&
2839
				$this->literal('{'))
2840
			{
2841
				$mixin = $this->pushSpecialBlock('mixin');
2842
				$mixin->name = $mixinName;
2843
				$mixin->args = $args;
2844
				return true;
2845
			} else {
2846
				$this->seek($s);
2847
			}
2848
2849
			if ($this->literal('@include') &&
2850
				$this->keyword($mixinName) &&
2851
				($this->literal('(') &&
2852
					($this->argValues($argValues) || true) &&
2853
					$this->literal(')') || true) &&
2854
				($this->end() ||
2855
					$this->literal('{') && $hasBlock = true))
2856
			{
2857
				$child = array('include',
2858
					$mixinName, isset($argValues) ? $argValues : null, null);
2859
2860
				if (!empty($hasBlock)) {
2861
					$include = $this->pushSpecialBlock('include');
2862
					$include->child = $child;
2863
				} else {
2864
					$this->append($child, $s);
2865
				}
2866
2867
				return true;
2868
			} else {
2869
				$this->seek($s);
2870
			}
2871
2872
			if ($this->literal('@import') &&
2873
				$this->valueList($importPath) &&
2874
				$this->end())
2875
			{
2876
				$this->append(array('import', $importPath), $s);
2877
				return true;
2878
			} else {
2879
				$this->seek($s);
2880
			}
2881
2882
			if ($this->literal('@extend') &&
2883
				$this->selectors($selector) &&
2884
				$this->end())
2885
			{
2886
				$this->append(array('extend', $selector), $s);
2887
				return true;
2888
			} else {
2889
				$this->seek($s);
2890
			}
2891
2892
			if ($this->literal('@function') &&
2893
				$this->keyword($fnName) &&
2894
				$this->argumentDef($args) &&
2895
				$this->literal('{'))
2896
			{
2897
				$func = $this->pushSpecialBlock('function');
2898
				$func->name = $fnName;
2899
				$func->args = $args;
2900
				return true;
2901
			} else {
2902
				$this->seek($s);
2903
			}
2904
2905
			if ($this->literal('@return') && $this->valueList($retVal) && $this->end()) {
2906
				$this->append(array('return', $retVal), $s);
2907
				return true;
2908
			} else {
2909
				$this->seek($s);
2910
			}
2911
2912
			if ($this->literal('@each') &&
2913
				$this->variable($varName) &&
2914
				$this->literal('in') &&
2915
				$this->valueList($list) &&
2916
				$this->literal('{'))
2917
			{
2918
				$each = $this->pushSpecialBlock('each');
2919
				$each->var = $varName[1];
2920
				$each->list = $list;
2921
				return true;
2922
			} else {
2923
				$this->seek($s);
2924
			}
2925
2926
			if ($this->literal('@while') &&
2927
				$this->expression($cond) &&
2928
				$this->literal('{'))
2929
			{
2930
				$while = $this->pushSpecialBlock('while');
2931
				$while->cond = $cond;
2932
				return true;
2933
			} else {
2934
				$this->seek($s);
2935
			}
2936
2937
			if ($this->literal('@for') &&
2938
				$this->variable($varName) &&
2939
				$this->literal('from') &&
2940
				$this->expression($start) &&
2941
				($this->literal('through') ||
2942
					($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...
2943
				$this->expression($end) &&
2944
				$this->literal('{'))
2945
			{
2946
				$for = $this->pushSpecialBlock('for');
2947
				$for->var = $varName[1];
2948
				$for->start = $start;
2949
				$for->end = $end;
2950
				$for->until = isset($forUntil);
2951
				return true;
2952
			} else {
2953
				$this->seek($s);
2954
			}
2955
2956
			if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) {
2957
				$if = $this->pushSpecialBlock('if');
2958
				$if->cond = $cond;
2959
				$if->cases = array();
2960
				return true;
2961
			} else {
2962
				$this->seek($s);
2963
			}
2964
2965
			if (($this->literal('@debug') || $this->literal('@warn')) &&
2966
				$this->valueList($value) &&
2967
				$this->end()) {
2968
				$this->append(array('debug', $value, $s), $s);
2969
				return true;
2970
			} else {
2971
				$this->seek($s);
2972
			}
2973
2974
			if ($this->literal('@content') && $this->end()) {
2975
				$this->append(array('mixin_content'), $s);
2976
				return true;
2977
			} else {
2978
				$this->seek($s);
2979
			}
2980
2981
			$last = $this->last();
2982
			if (isset($last) && $last[0] == 'if') {
2983
				list(, $if) = $last;
2984
				if ($this->literal('@else')) {
2985
					if ($this->literal('{')) {
2986
						$else = $this->pushSpecialBlock('else');
2987
					} elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) {
2988
						$else = $this->pushSpecialBlock('elseif');
2989
						$else->cond = $cond;
2990
					}
2991
2992
					if (isset($else)) {
2993
						$else->dontAppend = true;
2994
						$if->cases[] = $else;
2995
						return true;
2996
					}
2997
				}
2998
2999
				$this->seek($s);
3000
			}
3001
3002
			if ($this->literal('@charset') &&
3003
				$this->valueList($charset) && $this->end())
3004
			{
3005
				$this->append(array('charset', $charset), $s);
3006
				return true;
3007
			} else {
3008
				$this->seek($s);
3009
			}
3010
3011
			// doesn't match built in directive, do generic one
3012
			if ($this->literal('@', false) && $this->keyword($dirName) &&
3013
				($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
3014
				$this->literal('{'))
3015
			{
3016
				$directive = $this->pushSpecialBlock('directive');
3017
				$directive->name = $dirName;
3018
				if (isset($dirValue)) $directive->value = $dirValue;
3019
				return true;
3020
			}
3021
3022
			$this->seek($s);
3023
			return false;
3024
		}
3025
3026
		// property shortcut
3027
		// captures most properties before having to parse a selector
3028
		if ($this->keyword($name, false) &&
3029
			$this->literal(': ') &&
3030
			$this->valueList($value) &&
3031
			$this->end())
3032
		{
3033
			$name = array('string', '', array($name));
3034
			$this->append(array('assign', $name, $value), $s);
3035
			return true;
3036
		} else {
3037
			$this->seek($s);
3038
		}
3039
3040
		// variable assigns
3041
		if ($this->variable($name) &&
3042
			$this->literal(':') &&
3043
			$this->valueList($value) && $this->end())
3044
		{
3045
			// check for !default
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
3046
			$defaultVar = $value[0] == 'list' && $this->stripDefault($value);
3047
			$this->append(array('assign', $name, $value, $defaultVar), $s);
3048
			return true;
3049
		} else {
3050
			$this->seek($s);
3051
		}
3052
3053
		// misc
3054
		if ($this->literal('-->')) {
3055
			return true;
3056
		}
3057
3058
		// opening css block
3059
		if ($this->selectors($selectors) && $this->literal('{')) {
3060
			$b = $this->pushBlock($selectors);
3061
			return true;
3062
		} else {
3063
			$this->seek($s);
3064
		}
3065
3066
		// property assign, or nested assign
3067
		if ($this->propertyName($name) && $this->literal(':')) {
3068
			$foundSomething = false;
3069
			if ($this->valueList($value)) {
3070
				$this->append(array('assign', $name, $value), $s);
3071
				$foundSomething = true;
3072
			}
3073
3074
			if ($this->literal('{')) {
3075
				$propBlock = $this->pushSpecialBlock('nestedprop');
3076
				$propBlock->prefix = $name;
3077
				$foundSomething = true;
3078
			} elseif ($foundSomething) {
3079
				$foundSomething = $this->end();
3080
			}
3081
3082
			if ($foundSomething) {
3083
				return true;
3084
			}
3085
3086
			$this->seek($s);
3087
		} else {
3088
			$this->seek($s);
3089
		}
3090
3091
		// closing a block
3092
		if ($this->literal('}')) {
3093
			$block = $this->popBlock();
3094
			if (isset($block->type) && $block->type == 'include') {
3095
				$include = $block->child;
3096
				unset($block->child);
3097
				$include[3] = $block;
3098
				$this->append($include, $s);
3099
			} elseif (empty($block->dontAppend)) {
3100
				$type = isset($block->type) ? $block->type : 'block';
3101
				$this->append(array($type, $block), $s);
3102
			}
3103
			return true;
3104
		}
3105
3106
		// extra stuff
3107
		if ($this->literal(';') ||
3108
			$this->literal('<!--'))
3109
		{
3110
			return true;
3111
		}
3112
3113
		return false;
3114
	}
3115
3116
	protected function stripDefault(&$value) {
3117
		$def = end($value[2]);
3118
		if ($def[0] == 'keyword' && $def[1] == '!default') {
3119
			array_pop($value[2]);
3120
			$value = $this->flattenList($value);
3121
			return true;
3122
		}
3123
3124
		if ($def[0] == 'list') {
3125
			return $this->stripDefault($value[2][count($value[2]) - 1]);
3126
		}
3127
3128
		return false;
3129
	}
3130
3131
	protected function literal($what, $eatWhitespace = null) {
3132
		if (!isset($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault;
3133
3134
		// shortcut on single letter
3135
		if (!isset($what[1]) && isset($this->buffer[$this->count])) {
3136
			if ($this->buffer[$this->count] == $what) {
3137
				if (!$eatWhitespace) {
3138
					$this->count++;
3139
					return true;
3140
				}
3141
				// goes below...
3142
			} else {
3143
				return false;
3144
			}
3145
		}
3146
3147
		return $this->match($this->preg_quote($what), $m, $eatWhitespace);
3148
	}
3149
3150
	// tree builders
3151
3152
	protected function pushBlock($selectors) {
3153
		$b = new stdClass;
3154
		$b->parent = $this->env; // not sure if we need this yet
3155
3156
		$b->selectors = $selectors;
3157
		$b->comments = array();
3158
3159
		if (!$this->env) {
3160
			$b->children = array();
3161
		} elseif (empty($this->env->children)) {
3162
			$this->env->children = $this->env->comments;
3163
			$b->children = array();
3164
			$this->env->comments = array();
3165
		} else {
3166
			$b->children = $this->env->comments;
3167
			$this->env->comments = array();
3168
		}
3169
3170
		$this->env = $b;
3171
		return $b;
3172
	}
3173
3174
	protected function pushSpecialBlock($type) {
3175
		$block = $this->pushBlock(null);
3176
		$block->type = $type;
3177
		return $block;
3178
	}
3179
3180
	protected function popBlock() {
3181
		$block = $this->env;
3182
3183
		if (empty($block->parent)) {
3184
			$this->throwParseError('unexpected }');
3185
		}
3186
3187
		$this->env = $block->parent;
3188
		unset($block->parent);
3189
3190
		$comments = $block->comments;
3191
		if (count($comments)) {
3192
			$this->env->comments = $comments;
3193
			unset($block->comments);
3194
		}
3195
3196
		return $block;
3197
	}
3198
3199
	protected function appendComment($comment) {
3200
		$comment[1] = substr(preg_replace(array('/^\s+/m', '/^(.)/m'), array('', ' \1'), $comment[1]), 1);
3201
3202
		$this->env->comments[] = $comment;
3203
	}
3204
3205
	protected function append($statement, $pos=null) {
3206
		if ($pos != null) {
3207
			$statement[-1] = $pos;
3208
			if (!$this->rootParser) $statement[-2] = $this;
3209
		}
3210
3211
		$this->env->children[] = $statement;
3212
3213
		$comments = $this->env->comments;
3214
		if (count($comments)) {
3215
			$this->env->children = array_merge($this->env->children, $comments);
3216
			$this->env->comments = array();
3217
		}
3218
	}
3219
3220
	// last child that was appended
3221
	protected function last() {
3222
		$i = count($this->env->children) - 1;
3223
		if (isset($this->env->children[$i]))
3224
			return $this->env->children[$i];
3225
	}
3226
3227
	// high level parsers (they return parts of ast)
3228
3229
	protected function mediaQueryList(&$out) {
3230
		return $this->genericList($out, 'mediaQuery', ',', false);
3231
	}
3232
3233
	protected function mediaQuery(&$out) {
3234
		$s = $this->seek();
3235
3236
		$expressions = null;
3237
		$parts = array();
3238
3239
		if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) && $this->mixedKeyword($mediaType)) {
3240
			$prop = array('mediaType');
3241
			if (isset($only)) $prop[] = array('keyword', 'only');
3242
			if (isset($not)) $prop[] = array('keyword', 'not');
3243
			$media = array('list', '', array());
3244
			foreach ((array)$mediaType as $type) {
3245
				if (is_array($type)) {
3246
					$media[2][] = $type;
3247
				} else {
3248
					$media[2][] = array('keyword', $type);
3249
				}
3250
			}
3251
			$prop[] = $media;
3252
			$parts[] = $prop;
3253
		}
3254
3255
		if (empty($parts) || $this->literal('and')) {
3256
			$this->genericList($expressions, 'mediaExpression', 'and', false);
3257
			if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
3258
		}
3259
3260
		$out = $parts;
3261
		return true;
3262
	}
3263
3264
	protected function mediaExpression(&$out) {
3265
		$s = $this->seek();
3266
		$value = null;
3267
		if ($this->literal('(') &&
3268
			$this->expression($feature) &&
3269
			($this->literal(':') && $this->expression($value) || true) &&
3270
			$this->literal(')'))
3271
		{
3272
			$out = array('mediaExp', $feature);
3273
			if ($value) $out[] = $value;
3274
			return true;
3275
		}
3276
3277
		$this->seek($s);
3278
		return false;
3279
	}
3280
3281
	protected function argValues(&$out) {
3282
		if ($this->genericList($list, 'argValue', ',', false)) {
3283
			$out = $list[2];
3284
			return true;
3285
		}
3286
		return false;
3287
	}
3288
3289
	protected function argValue(&$out) {
3290
		$s = $this->seek();
3291
3292
		$keyword = null;
3293
		if (!$this->variable($keyword) || !$this->literal(':')) {
3294
			$this->seek($s);
3295
			$keyword = null;
3296
		}
3297
3298
		if ($this->genericList($value, 'expression')) {
3299
			$out = array($keyword, $value, false);
3300
			$s = $this->seek();
3301
			if ($this->literal('...')) {
3302
				$out[2] = true;
3303
			} else {
3304
				$this->seek($s);
3305
			}
3306
			return true;
3307
		}
3308
3309
		return false;
3310
	}
3311
3312
	/**
3313
	 * Parse list
3314
	 *
3315
	 * @param string $out
3316
	 *
3317
	 * @return boolean
3318
	 */
3319
	public function valueList(&$out)
3320
	{
3321
		return $this->genericList($out, 'spaceList', ',');
3322
	}
3323
3324
	protected function spaceList(&$out)
3325
	{
3326
		return $this->genericList($out, 'expression');
3327
	}
3328
3329
	protected function genericList(&$out, $parseItem, $delim='', $flatten=true) {
3330
		$s = $this->seek();
3331
		$items = array();
3332
		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...
3333
			$items[] = $value;
3334
			if ($delim) {
3335
				if (!$this->literal($delim)) break;
3336
			}
3337
		}
3338
3339
		if (count($items) == 0) {
3340
			$this->seek($s);
3341
			return false;
3342
		}
3343
3344
		if ($flatten && count($items) == 1) {
3345
			$out = $items[0];
3346
		} else {
3347
			$out = array('list', $delim, $items);
3348
		}
3349
3350
		return true;
3351
	}
3352
3353
	protected function expression(&$out) {
3354
		$s = $this->seek();
3355
3356
		if ($this->literal('(')) {
3357
			if ($this->literal(')')) {
3358
				$out = array('list', '', array());
3359
				return true;
3360
			}
3361
3362
			if ($this->valueList($out) && $this->literal(')') && $out[0] == 'list') {
3363
				return true;
3364
			}
3365
3366
			$this->seek($s);
3367
		}
3368
3369
		if ($this->value($lhs)) {
3370
			$out = $this->expHelper($lhs, 0);
3371
			return true;
3372
		}
3373
3374
		return false;
3375
	}
3376
3377
	protected function expHelper($lhs, $minP) {
3378
		$opstr = self::$operatorStr;
3379
3380
		$ss = $this->seek();
3381
		$whiteBefore = isset($this->buffer[$this->count - 1]) &&
3382
			ctype_space($this->buffer[$this->count - 1]);
3383
		while ($this->match($opstr, $m) && self::$precedence[$m[1]] >= $minP) {
3384
			$whiteAfter = isset($this->buffer[$this->count - 1]) &&
3385
				ctype_space($this->buffer[$this->count - 1]);
3386
3387
			$op = $m[1];
3388
3389
			// don't turn negative numbers into expressions
3390
			if ($op == '-' && $whiteBefore) {
3391
				if (!$whiteAfter) break;
3392
			}
3393
3394
			if (!$this->value($rhs)) break;
3395
3396
			// peek and see if rhs belongs to next operator
3397
			if ($this->peek($opstr, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
3398
				$rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
3399
			}
3400
3401
			$lhs = array('exp', $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter);
3402
			$ss = $this->seek();
3403
			$whiteBefore = isset($this->buffer[$this->count - 1]) &&
3404
				ctype_space($this->buffer[$this->count - 1]);
3405
		}
3406
3407
		$this->seek($ss);
3408
		return $lhs;
3409
	}
3410
3411
	protected function value(&$out) {
3412
		$s = $this->seek();
3413
3414
		if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) {
3415
			$out = array('unary', 'not', $inner, $this->inParens);
3416
			return true;
3417
		} else {
3418
			$this->seek($s);
3419
		}
3420
3421
		if ($this->literal('+') && $this->value($inner)) {
3422
			$out = array('unary', '+', $inner, $this->inParens);
3423
			return true;
3424
		} else {
3425
			$this->seek($s);
3426
		}
3427
3428
		// negation
3429
		if ($this->literal('-', false) &&
3430
			($this->variable($inner) ||
3431
			$this->unit($inner) ||
3432
			$this->parenValue($inner)))
3433
		{
3434
			$out = array('unary', '-', $inner, $this->inParens);
3435
			return true;
3436
		} else {
3437
			$this->seek($s);
3438
		}
3439
3440
		if ($this->parenValue($out)) return true;
3441
		if ($this->interpolation($out)) return true;
3442
		if ($this->variable($out)) return true;
3443
		if ($this->color($out)) return true;
3444
		if ($this->unit($out)) return true;
3445
		if ($this->string($out)) return true;
3446
		if ($this->func($out)) return true;
3447
		if ($this->progid($out)) return true;
3448
3449
		if ($this->keyword($keyword)) {
3450
			if ($keyword == 'null') {
3451
				$out = array('null');
3452
			} else {
3453
				$out = array('keyword', $keyword);
3454
			}
3455
			return true;
3456
		}
3457
3458
		return false;
3459
	}
3460
3461
	// value wrappen in parentheses
3462
	protected function parenValue(&$out) {
3463
		$s = $this->seek();
3464
3465
		$inParens = $this->inParens;
3466
		if ($this->literal('(') &&
3467
			($this->inParens = true) && $this->expression($exp) &&
3468
			$this->literal(')'))
3469
		{
3470
			$out = $exp;
3471
			$this->inParens = $inParens;
3472
			return true;
3473
		} else {
3474
			$this->inParens = $inParens;
3475
			$this->seek($s);
3476
		}
3477
3478
		return false;
3479
	}
3480
3481
	protected function progid(&$out) {
3482
		$s = $this->seek();
3483
		if ($this->literal('progid:', false) &&
3484
			$this->openString('(', $fn) &&
3485
			$this->literal('('))
3486
		{
3487
			$this->openString(')', $args, '(');
3488
			if ($this->literal(')')) {
3489
				$out = array('string', '', array(
3490
					'progid:', $fn, '(', $args, ')'
3491
				));
3492
				return true;
3493
			}
3494
		}
3495
3496
		$this->seek($s);
3497
		return false;
3498
	}
3499
3500
	protected function func(&$func) {
3501
		$s = $this->seek();
3502
3503
		if ($this->keyword($name, false) &&
3504
			$this->literal('('))
3505
		{
3506
			if ($name == 'alpha' && $this->argumentList($args)) {
3507
				$func = array('function', $name, array('string', '', $args));
3508
				return true;
3509
			}
3510
3511
			if ($name != 'expression' && !preg_match('/^(-[a-z]+-)?calc$/', $name)) {
3512
				$ss = $this->seek();
3513
				if ($this->argValues($args) && $this->literal(')')) {
3514
					$func = array('fncall', $name, $args);
3515
					return true;
3516
				}
3517
				$this->seek($ss);
3518
			}
3519
3520
			if (($this->openString(')', $str, '(') || true ) &&
3521
				$this->literal(')'))
3522
			{
3523
				$args = array();
3524
				if (!empty($str)) {
3525
					$args[] = array(null, array('string', '', array($str)));
3526
				}
3527
3528
				$func = array('fncall', $name, $args);
3529
				return true;
3530
			}
3531
		}
3532
3533
		$this->seek($s);
3534
		return false;
3535
	}
3536
3537
	protected function argumentList(&$out) {
3538
		$s = $this->seek();
3539
		$this->literal('(');
3540
3541
		$args = array();
3542
		while ($this->keyword($var)) {
3543
			$ss = $this->seek();
3544
3545
			if ($this->literal('=') && $this->expression($exp)) {
3546
				$args[] = array('string', '', array($var.'='));
3547
				$arg = $exp;
3548
			} else {
3549
				break;
3550
			}
3551
3552
			$args[] = $arg;
3553
3554
			if (!$this->literal(',')) break;
3555
3556
			$args[] = array('string', '', array(', '));
3557
		}
3558
3559
		if (!$this->literal(')') || !count($args)) {
3560
			$this->seek($s);
3561
			return false;
3562
		}
3563
3564
		$out = $args;
3565
		return true;
3566
	}
3567
3568
	protected function argumentDef(&$out) {
3569
		$s = $this->seek();
3570
		$this->literal('(');
3571
3572
		$args = array();
3573
		while ($this->variable($var)) {
3574
			$arg = array($var[1], null, false);
3575
3576
			$ss = $this->seek();
3577
			if ($this->literal(':') && $this->genericList($defaultVal, 'expression')) {
3578
				$arg[1] = $defaultVal;
3579
			} else {
3580
				$this->seek($ss);
3581
			}
3582
3583
			$ss = $this->seek();
3584
			if ($this->literal('...')) {
3585
				$sss = $this->seek();
3586
				if (!$this->literal(')')) {
3587
					$this->throwParseError('... has to be after the final argument');
3588
				}
3589
				$arg[2] = true;
3590
				$this->seek($sss);
3591
			} else {
3592
				$this->seek($ss);
3593
			}
3594
3595
			$args[] = $arg;
3596
			if (!$this->literal(',')) break;
3597
		}
3598
3599
		if (!$this->literal(')')) {
3600
			$this->seek($s);
3601
			return false;
3602
		}
3603
3604
		$out = $args;
3605
		return true;
3606
	}
3607
3608
	protected function color(&$out) {
3609
		$color = array('color');
3610
3611
		if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
3612
			if (isset($m[3])) {
3613
				$num = $m[3];
3614
				$width = 16;
3615
			} else {
3616
				$num = $m[2];
3617
				$width = 256;
3618
			}
3619
3620
			$num = hexdec($num);
3621
			foreach (array(3,2,1) as $i) {
3622
				$t = $num % $width;
3623
				$num /= $width;
3624
3625
				$color[$i] = $t * (256/$width) + $t * floor(16/$width);
3626
			}
3627
3628
			$out = $color;
3629
			return true;
3630
		}
3631
3632
		return false;
3633
	}
3634
3635
	protected function unit(&$unit) {
3636
		if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
3637
			$unit = array('number', $m[1], empty($m[3]) ? '' : $m[3]);
3638
			return true;
3639
		}
3640
		return false;
3641
	}
3642
3643
	protected function string(&$out) {
3644
		$s = $this->seek();
3645
		if ($this->literal('"', false)) {
3646
			$delim = '"';
3647
		} elseif ($this->literal('\'', false)) {
3648
			$delim = '\'';
3649
		} else {
3650
			return false;
3651
		}
3652
3653
		$content = array();
3654
		$oldWhite = $this->eatWhiteDefault;
3655
		$this->eatWhiteDefault = false;
3656
3657
		while ($this->matchString($m, $delim)) {
3658
			$content[] = $m[1];
3659
			if ($m[2] == '#{') {
3660
				$this->count -= strlen($m[2]);
3661
				if ($this->interpolation($inter, false)) {
3662
					$content[] = $inter;
3663
				} else {
3664
					$this->count += strlen($m[2]);
3665
					$content[] = '#{'; // ignore it
3666
				}
3667
			} elseif ($m[2] == '\\') {
3668
				$content[] = $m[2];
3669
				if ($this->literal($delim, false)) {
3670
					$content[] = $delim;
3671
				}
3672
			} else {
3673
				$this->count -= strlen($delim);
3674
				break; // delim
3675
			}
3676
		}
3677
3678
		$this->eatWhiteDefault = $oldWhite;
3679
3680
		if ($this->literal($delim)) {
3681
			$out = array('string', $delim, $content);
3682
			return true;
3683
		}
3684
3685
		$this->seek($s);
3686
		return false;
3687
	}
3688
3689
	protected function mixedKeyword(&$out) {
3690
		$s = $this->seek();
3691
3692
		$parts = array();
3693
3694
		$oldWhite = $this->eatWhiteDefault;
3695
		$this->eatWhiteDefault = false;
3696
3697
		while (true) {
3698
			if ($this->keyword($key)) {
3699
				$parts[] = $key;
3700
				continue;
3701
			}
3702
3703
			if ($this->interpolation($inter)) {
3704
				$parts[] = $inter;
3705
				continue;
3706
			}
3707
3708
			break;
3709
		}
3710
3711
		$this->eatWhiteDefault = $oldWhite;
3712
3713
		if (count($parts) == 0) return false;
3714
3715
		if ($this->eatWhiteDefault) {
3716
			$this->whitespace();
3717
		}
3718
3719
		$out = $parts;
3720
		return true;
3721
	}
3722
3723
	// an unbounded string stopped by $end
3724
	protected function openString($end, &$out, $nestingOpen=null) {
3725
		$oldWhite = $this->eatWhiteDefault;
3726
		$this->eatWhiteDefault = false;
3727
3728
		$stop = array('\'', '"', '#{', $end);
3729
		$stop = array_map(array($this, 'preg_quote'), $stop);
3730
		$stop[] = self::$commentMulti;
3731
3732
		$patt = '(.*?)('.implode('|', $stop).')';
3733
3734
		$nestingLevel = 0;
3735
3736
		$content = array();
3737
		while ($this->match($patt, $m, false)) {
3738
			if (isset($m[1]) && $m[1] != '') {
3739
				$content[] = $m[1];
3740
				if ($nestingOpen) {
3741
					$nestingLevel += substr_count($m[1], $nestingOpen);
3742
				}
3743
			}
3744
3745
			$tok = $m[2];
3746
3747
			$this->count-= strlen($tok);
3748
			if ($tok == $end) {
3749
				if ($nestingLevel == 0) {
3750
					break;
3751
				} else {
3752
					$nestingLevel--;
3753
				}
3754
			}
3755
3756
			if (($tok == '\'' || $tok == '"') && $this->string($str)) {
3757
				$content[] = $str;
3758
				continue;
3759
			}
3760
3761
			if ($tok == '#{' && $this->interpolation($inter)) {
3762
				$content[] = $inter;
3763
				continue;
3764
			}
3765
3766
			$content[] = $tok;
3767
			$this->count+= strlen($tok);
3768
		}
3769
3770
		$this->eatWhiteDefault = $oldWhite;
3771
3772
		if (count($content) == 0) return false;
3773
3774
		// trim the end
3775
		if (is_string(end($content))) {
3776
			$content[count($content) - 1] = rtrim(end($content));
3777
		}
3778
3779
		$out = array('string', '', $content);
3780
		return true;
3781
	}
3782
3783
	// $lookWhite: save information about whitespace before and after
3784
	protected function interpolation(&$out, $lookWhite=true) {
3785
		$oldWhite = $this->eatWhiteDefault;
3786
		$this->eatWhiteDefault = true;
3787
3788
		$s = $this->seek();
3789
		if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) {
3790
3791
			// TODO: don't error if out of bounds
3792
3793
			if ($lookWhite) {
3794
				$left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
3795
				$right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
3796
			} else {
3797
				$left = $right = false;
3798
			}
3799
3800
			$out = array('interpolate', $value, $left, $right);
3801
			$this->eatWhiteDefault = $oldWhite;
3802
			if ($this->eatWhiteDefault) {
3803
				$this->whitespace();
3804
			}
3805
			return true;
3806
		}
3807
3808
		$this->seek($s);
3809
		$this->eatWhiteDefault = $oldWhite;
3810
		return false;
3811
	}
3812
3813
	// low level parsers
3814
3815
	// returns an array of parts or a string
3816
	protected function propertyName(&$out) {
3817
		$s = $this->seek();
3818
		$parts = array();
3819
3820
		$oldWhite = $this->eatWhiteDefault;
3821
		$this->eatWhiteDefault = false;
3822
3823
		while (true) {
3824
			if ($this->interpolation($inter)) {
3825
				$parts[] = $inter;
3826
			} elseif ($this->keyword($text)) {
3827
				$parts[] = $text;
3828
			} elseif (count($parts) == 0 && $this->match('[:.#]', $m, false)) {
3829
				// css hacks
3830
				$parts[] = $m[0];
3831
			} else {
3832
				break;
3833
			}
3834
		}
3835
3836
		$this->eatWhiteDefault = $oldWhite;
3837
		if (count($parts) == 0) return false;
3838
3839
		// match comment hack
3840
		if (preg_match(self::$whitePattern,
3841
			$this->buffer, $m, null, $this->count))
3842
		{
3843
			if (!empty($m[0])) {
3844
				$parts[] = $m[0];
3845
				$this->count += strlen($m[0]);
3846
			}
3847
		}
3848
3849
		$this->whitespace(); // get any extra whitespace
3850
3851
		$out = array('string', '', $parts);
3852
		return true;
3853
	}
3854
3855
	// comma separated list of selectors
3856
	protected function selectors(&$out) {
3857
		$s = $this->seek();
3858
		$selectors = array();
3859
		while ($this->selector($sel)) {
3860
			$selectors[] = $sel;
3861
			if (!$this->literal(',')) break;
3862
			while ($this->literal(',')); // ignore extra
3863
		}
3864
3865
		if (count($selectors) == 0) {
3866
			$this->seek($s);
3867
			return false;
3868
		}
3869
3870
		$out = $selectors;
3871
		return true;
3872
	}
3873
3874
	// whitespace separated list of selectorSingle
3875
	protected function selector(&$out) {
3876
		$selector = array();
3877
3878
		while (true) {
3879
			if ($this->match('[>+~]+', $m)) {
3880
				$selector[] = array($m[0]);
3881
			} elseif ($this->selectorSingle($part)) {
3882
				$selector[] = $part;
3883
				$this->match('\s+', $m);
3884
			} elseif ($this->match('\/[^\/]+\/', $m)) {
3885
				$selector[] = array($m[0]);
3886
			} else {
3887
				break;
3888
			}
3889
3890
		}
3891
3892
		if (count($selector) == 0) {
3893
			return false;
3894
		}
3895
3896
		$out = $selector;
3897
		return true;
3898
	}
3899
3900
	// the parts that make up
3901
	// div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
3902
	protected function selectorSingle(&$out) {
3903
		$oldWhite = $this->eatWhiteDefault;
3904
		$this->eatWhiteDefault = false;
3905
3906
		$parts = array();
3907
3908
		if ($this->literal('*', false)) {
3909
			$parts[] = '*';
3910
		}
3911
3912
		while (true) {
3913
			// see if we can stop early
3914
			if ($this->match('\s*[{,]', $m)) {
3915
				$this->count--;
3916
				break;
3917
			}
3918
3919
			$s = $this->seek();
3920
			// self
3921
			if ($this->literal('&', false)) {
3922
				$parts[] = titanscssc::$selfSelector;
3923
				continue;
3924
			}
3925
3926
			if ($this->literal('.', false)) {
3927
				$parts[] = '.';
3928
				continue;
3929
			}
3930
3931
			if ($this->literal('|', false)) {
3932
				$parts[] = '|';
3933
				continue;
3934
			}
3935
3936
			if ($this->match('\\\\\S', $m)) {
3937
				$parts[] = $m[0];
3938
				continue;
3939
			}
3940
3941
			// for keyframes
3942
			if ($this->unit($unit)) {
3943
				$parts[] = $unit;
3944
				continue;
3945
			}
3946
3947
			if ($this->keyword($name)) {
3948
				$parts[] = $name;
3949
				continue;
3950
			}
3951
3952
			if ($this->interpolation($inter)) {
3953
				$parts[] = $inter;
3954
				continue;
3955
			}
3956
3957
			if ($this->literal('%', false) && $this->placeholder($placeholder)) {
3958
				$parts[] = '%';
3959
				$parts[] = $placeholder;
3960
				continue;
3961
			}
3962
3963
			if ($this->literal('#', false)) {
3964
				$parts[] = '#';
3965
				continue;
3966
			}
3967
3968
			// a pseudo selector
3969
			if ($this->match('::?', $m) && $this->mixedKeyword($nameParts)) {
3970
				$parts[] = $m[0];
3971
				foreach ($nameParts as $sub) {
3972
					$parts[] = $sub;
3973
				}
3974
3975
				$ss = $this->seek();
3976
				if ($this->literal('(') &&
3977
					($this->openString(')', $str, '(') || true ) &&
3978
					$this->literal(')'))
3979
				{
3980
					$parts[] = '(';
3981
					if (!empty($str)) $parts[] = $str;
3982
					$parts[] = ')';
3983
				} else {
3984
					$this->seek($ss);
3985
				}
3986
3987
				continue;
3988
			} else {
3989
				$this->seek($s);
3990
			}
3991
3992
			// attribute selector
3993
			// TODO: replace with open string?
3994
			if ($this->literal('[', false)) {
3995
				$attrParts = array('[');
3996
				// keyword, string, operator
3997
				while (true) {
3998
					if ($this->literal(']', false)) {
3999
						$this->count--;
4000
						break; // get out early
4001
					}
4002
4003
					if ($this->match('\s+', $m)) {
4004
						$attrParts[] = ' ';
4005
						continue;
4006
					}
4007
					if ($this->string($str)) {
4008
						$attrParts[] = $str;
4009
						continue;
4010
					}
4011
4012
					if ($this->keyword($word)) {
4013
						$attrParts[] = $word;
4014
						continue;
4015
					}
4016
4017
					if ($this->interpolation($inter, false)) {
4018
						$attrParts[] = $inter;
4019
						continue;
4020
					}
4021
4022
					// operator, handles attr namespace too
4023
					if ($this->match('[|-~\$\*\^=]+', $m)) {
4024
						$attrParts[] = $m[0];
4025
						continue;
4026
					}
4027
4028
					break;
4029
				}
4030
4031
				if ($this->literal(']', false)) {
4032
					$attrParts[] = ']';
4033
					foreach ($attrParts as $part) {
4034
						$parts[] = $part;
4035
					}
4036
					continue;
4037
				}
4038
				$this->seek($s);
4039
				// should just break here?
4040
			}
4041
4042
			break;
4043
		}
4044
4045
		$this->eatWhiteDefault = $oldWhite;
4046
4047
		if (count($parts) == 0) return false;
4048
4049
		$out = $parts;
4050
		return true;
4051
	}
4052
4053
	protected function variable(&$out) {
4054
		$s = $this->seek();
4055
		if ($this->literal('$', false) && $this->keyword($name)) {
4056
			$out = array('var', $name);
4057
			return true;
4058
		}
4059
		$this->seek($s);
4060
		return false;
4061
	}
4062
4063
	protected function keyword(&$word, $eatWhitespace = null) {
4064
		if ($this->match('(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
4065
			$m, $eatWhitespace))
4066
		{
4067
			$word = $m[1];
4068
			return true;
4069
		}
4070
		return false;
4071
	}
4072
4073
	protected function placeholder(&$placeholder) {
4074
		if ($this->match('([\w\-_]+)', $m)) {
4075
			$placeholder = $m[1];
4076
			return true;
4077
		}
4078
		return false;
4079
	}
4080
4081
	// consume an end of statement delimiter
4082
	protected function end() {
4083
		if ($this->literal(';')) {
4084
			return true;
4085
		} elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
4086
			// if there is end of file or a closing block next then we don't need a ;
4087
			return true;
4088
		}
4089
		return false;
4090
	}
4091
4092
	// advance counter to next occurrence of $what
4093
	// $until - don't include $what in advance
4094
	// $allowNewline, if string, will be used as valid char set
4095
	protected function to($what, &$out, $until = false, $allowNewline = false) {
4096
		if (is_string($allowNewline)) {
4097
			$validChars = $allowNewline;
4098
		} else {
4099
			$validChars = $allowNewline ? '.' : "[^\n]";
4100
		}
4101
		if (!$this->match('('.$validChars.'*?)'.$this->preg_quote($what), $m, !$until)) return false;
4102
		if ($until) $this->count -= strlen($what); // give back $what
4103
		$out = $m[1];
4104
		return true;
4105
	}
4106
4107
	public function throwParseError($msg = 'parse error', $count = null) {
4108
		$count = !isset($count) ? $this->count : $count;
4109
4110
		$line = $this->getLineNo($count);
4111
4112
		if (!empty($this->sourceName)) {
4113
			$loc = "$this->sourceName on line $line";
4114
		} else {
4115
			$loc = "line: $line";
4116
		}
4117
4118
		if ($this->peek("(.*?)(\n|$)", $m, $count)) {
4119
			throw new Exception("$msg: failed at `$m[1]` $loc");
4120
		} else {
4121
			throw new Exception("$msg: $loc");
4122
		}
4123
	}
4124
4125
	public function getLineNo($pos) {
4126
		return 1 + substr_count(substr($this->buffer, 0, $pos), "\n");
4127
	}
4128
4129
	/**
4130
	 * Match string looking for either ending delim, escape, or string interpolation
4131
	 *
4132
	 * {@internal This is a workaround for preg_match's 250K string match limit. }}
4133
	 *
4134
	 * @param array  $m     Matches (passed by reference)
4135
	 * @param string $delim Delimeter
4136
	 *
4137
	 * @return boolean True if match; false otherwise
4138
	 */
4139
	protected function matchString(&$m, $delim) {
4140
		$token = null;
4141
4142
		$end = strpos($this->buffer, "\n", $this->count);
4143
		if ($end == false || $this->buffer[$end - 1] == '\\' || $this->buffer[$end - 2] == '\\' && $this->buffer[$end - 1] == "\r") {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $end of type integer to the boolean false. If you are specifically checking for 0, consider using something more explicit like === 0 instead.
Loading history...
4144
			$end = strlen($this->buffer);
4145
		}
4146
4147
		// look for either ending delim, escape, or string interpolation
4148
		foreach (array('#{', '\\', $delim) as $lookahead) {
4149
			$pos = strpos($this->buffer, $lookahead, $this->count);
4150
			if ($pos != false && $pos < $end) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $pos of type integer to the boolean false. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
4151
				$end = $pos;
4152
				$token = $lookahead;
4153
			}
4154
		}
4155
4156
		if (!isset($token)) {
4157
			return false;
4158
		}
4159
4160
		$match = substr($this->buffer, $this->count, $end - $this->count);
4161
		$m = array(
4162
			$match . $token,
4163
			$match,
4164
			$token
4165
		);
4166
		$this->count = $end + strlen($token);
4167
4168
		return true;
4169
	}
4170
4171
	// try to match something on head of buffer
4172
	protected function match($regex, &$out, $eatWhitespace = null) {
4173
		if (!isset($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault;
4174
4175
		$r = '/'.$regex.'/Ais';
4176
		if (preg_match($r, $this->buffer, $out, null, $this->count)) {
4177
			$this->count += strlen($out[0]);
4178
			if ($eatWhitespace) {
4179
				$this->whitespace();
4180
			}
4181
			return true;
4182
		}
4183
		return false;
4184
	}
4185
4186
	// match some whitespace
4187
	protected function whitespace() {
4188
		$gotWhite = false;
4189
		while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
4190
			if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
4191
				$this->appendComment(array('comment', $m[1]));
4192
				$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...
4193
			}
4194
			$this->count += strlen($m[0]);
4195
			$gotWhite = true;
4196
		}
4197
		return $gotWhite;
4198
	}
4199
4200
	protected function peek($regex, &$out, $from=null) {
4201
		if (!isset($from)) $from = $this->count;
4202
4203
		$r = '/'.$regex.'/Ais';
4204
		$result = preg_match($r, $this->buffer, $out, null, $from);
4205
4206
		return $result;
4207
	}
4208
4209
	protected function seek($where = null) {
4210
		if ($where == null) return $this->count;
4211
		else $this->count = $where;
4212
		return true;
4213
	}
4214
4215
	static function preg_quote($what) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
4216
		return preg_quote($what, '/');
4217
	}
4218
4219
	protected function show() {
4220
		if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
4221
			return $m[1];
4222
		}
4223
		return '';
4224
	}
4225
4226
	// turn list of length 1 into value type
4227
	protected function flattenList($value) {
4228
		if ($value[0] == 'list' && count($value[2]) == 1) {
4229
			return $this->flattenList($value[2][0]);
4230
		}
4231
		return $value;
4232
	}
4233
}
4234
4235
/**
4236
 * SCSS base formatter
4237
 *
4238
 * @author Leaf Corcoran <[email protected]>
4239
 */
4240
class titanscss_formatter {
4241
	public $indentChar = '  ';
4242
4243
	public $break = "\n";
4244
	public $open = ' {';
4245
	public $close = '}';
4246
	public $tagSeparator = ', ';
4247
	public $assignSeparator = ': ';
4248
4249
	public function __construct() {
4250
		$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...
4251
	}
4252
4253
	public function indentStr($n = 0) {
4254
		return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
4255
	}
4256
4257
	public function property($name, $value) {
4258
		return $name . $this->assignSeparator . $value . ';';
4259
	}
4260
4261
	protected function blockLines($inner, $block)
4262
	{
4263
		$glue = $this->break.$inner;
4264
		echo $inner . implode($glue, $block->lines);
4265
4266
		if (!empty($block->children)) {
4267
			echo $this->break;
4268
		}
4269
	}
4270
4271
	protected function block($block) {
4272
		if (empty($block->lines) && empty($block->children)) return;
4273
4274
		$inner = $pre = $this->indentStr();
4275
4276
		if (!empty($block->selectors)) {
4277
			echo $pre .
4278
				implode($this->tagSeparator, $block->selectors) .
4279
				$this->open . $this->break;
4280
			$this->indentLevel++;
4281
			$inner = $this->indentStr();
4282
		}
4283
4284
		if (!empty($block->lines)) {
4285
			$this->blockLines($inner, $block);
4286
		}
4287
4288
		foreach ($block->children as $child) {
4289
			$this->block($child);
4290
		}
4291
4292
		if (!empty($block->selectors)) {
4293
			$this->indentLevel--;
4294
			if (empty($block->children)) echo $this->break;
4295
			echo $pre . $this->close . $this->break;
4296
		}
4297
	}
4298
4299
	public function format($block) {
4300
		ob_start();
4301
		$this->block($block);
4302
		$out = ob_get_clean();
4303
4304
		return $out;
4305
	}
4306
}
4307
4308
/**
4309
 * SCSS nested formatter
4310
 *
4311
 * @author Leaf Corcoran <[email protected]>
4312
 */
4313
class titanscss_formatter_nested extends titanscss_formatter {
4314
	public $close = ' }';
4315
4316
	// adjust the depths of all children, depth first
4317
	public function adjustAllChildren($block) {
4318
		// flatten empty nested blocks
4319
		$children = array();
4320
		foreach ($block->children as $i => $child) {
4321
			if (empty($child->lines) && empty($child->children)) {
4322
				if (isset($block->children[$i + 1])) {
4323
					$block->children[$i + 1]->depth = $child->depth;
4324
				}
4325
				continue;
4326
			}
4327
			$children[] = $child;
4328
		}
4329
4330
		$count = count($children);
4331
		for ($i = 0; $i < $count; $i++) {
4332
			$depth = $children[$i]->depth;
4333
			$j = $i + 1;
4334
			if (isset($children[$j]) && $depth < $children[$j]->depth) {
4335
				$childDepth = $children[$j]->depth;
4336
				for (; $j < $count; $j++) {
4337
					if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) {
4338
						$children[$j]->depth = $depth + 1;
4339
					}
4340
				}
4341
			}
4342
		}
4343
4344
		$block->children = $children;
4345
4346
		// make relative to parent
4347
		foreach ($block->children as $child) {
4348
			$this->adjustAllChildren($child);
4349
			$child->depth = $child->depth - $block->depth;
4350
		}
4351
	}
4352
4353
	protected function blockLines($inner, $block)
4354
	{
4355
		$glue = $this->break . $inner;
4356
4357
		foreach ($block->lines as $index => $line) {
4358
			if (substr($line, 0, 2) == '/*') {
4359
				$block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line);
4360
			}
4361
		}
4362
4363
		echo $inner . implode($glue, $block->lines);
4364
4365
		if (!empty($block->children)) {
4366
			echo $this->break;
4367
		}
4368
	}
4369
4370
	protected function block($block) {
4371
		if ($block->type == 'root') {
4372
			$this->adjustAllChildren($block);
4373
		}
4374
4375
		$inner = $pre = $this->indentStr($block->depth - 1);
4376
		if (!empty($block->selectors)) {
4377
			echo $pre .
4378
				implode($this->tagSeparator, $block->selectors) .
4379
				$this->open . $this->break;
4380
			$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...
4381
			$inner = $this->indentStr($block->depth - 1);
4382
		}
4383
4384
		if (!empty($block->lines)) {
4385
			$this->blockLines($inner, $block);
4386
		}
4387
4388
		foreach ($block->children as $i => $child) {
4389
			// echo "*** block: ".$block->depth." child: ".$child->depth."\n";
0 ignored issues
show
Unused Code Comprehensibility introduced by
53% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
4390
			$this->block($child);
4391
			if ($i < count($block->children) - 1) {
4392
				echo $this->break;
4393
4394
				if (isset($block->children[$i + 1])) {
4395
					$next = $block->children[$i + 1];
4396
					if ($next->depth == max($block->depth, 1) && $child->depth >= $next->depth) {
4397
						echo $this->break;
4398
					}
4399
				}
4400
			}
4401
		}
4402
4403
		if (!empty($block->selectors)) {
4404
			$this->indentLevel--;
4405
			echo $this->close;
4406
		}
4407
4408
		if ($block->type == 'root') {
4409
			echo $this->break;
4410
		}
4411
	}
4412
}
4413
4414
/**
4415
 * SCSS compressed formatter
4416
 *
4417
 * @author Leaf Corcoran <[email protected]>
4418
 */
4419
class titanscss_formatter_compressed extends titanscss_formatter {
4420
	public $open = '{';
4421
	public $tagSeparator = ',';
4422
	public $assignSeparator = ':';
4423
	public $break = '';
4424
4425
	public function indentStr($n = 0) {
4426
		return '';
4427
	}
4428
4429
	public function blockLines($inner, $block)
4430
	{
4431
		$glue = $this->break.$inner;
4432
4433
		foreach ($block->lines as $index => $line) {
4434
			if (substr($line, 0, 2) == '/*' && substr($line, 2, 1) != '!') {
4435
				unset($block->lines[$index]);
4436
			} elseif (substr($line, 0, 3) == '/*!') {
4437
				$block->lines[$index] = '/*' . substr($line, 3);
4438
			}
4439
		}
4440
4441
		echo $inner . implode($glue, $block->lines);
4442
4443
		if (!empty($block->children)) {
4444
			echo $this->break;
4445
		}
4446
	}
4447
}
4448
4449
/**
4450
 * SCSS crunched formatter
4451
 *
4452
 * @author Anthon Pang <[email protected]>
4453
 */
4454
class titanscss_formatter_crunched extends titanscss_formatter {
4455
	public $open = '{';
4456
	public $tagSeparator = ',';
4457
	public $assignSeparator = ':';
4458
	public $break = '';
4459
4460
	public function indentStr($n = 0) {
4461
		return '';
4462
	}
4463
4464
	public function blockLines($inner, $block)
4465
	{
4466
		$glue = $this->break.$inner;
4467
4468
		foreach ($block->lines as $index => $line) {
4469
			if (substr($line, 0, 2) == '/*') {
4470
				unset($block->lines[$index]);
4471
			}
4472
		}
4473
4474
		echo $inner . implode($glue, $block->lines);
4475
4476
		if (!empty($block->children)) {
4477
			echo $this->break;
4478
		}
4479
	}
4480
}
4481
4482
/**
4483
 * SCSS server
4484
 *
4485
 * @author Leaf Corcoran <[email protected]>
4486
 */
4487
class titanscss_server {
4488
	/**
4489
	 * Join path components
4490
	 *
4491
	 * @param string $left  Path component, left of the directory separator
4492
	 * @param string $right Path component, right of the directory separator
4493
	 *
4494
	 * @return string
4495
	 */
4496
	protected function join($left, $right) {
4497
		return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\');
4498
	}
4499
4500
	/**
4501
	 * Get name of requested .scss file
4502
	 *
4503
	 * @return string|null
4504
	 */
4505
	protected function inputName() {
4506
		switch (true) {
4507
			case isset($_GET['p']):
4508
				return $_GET['p'];
4509
			case isset($_SERVER['PATH_INFO']):
4510
				return $_SERVER['PATH_INFO'];
4511
			case isset($_SERVER['DOCUMENT_URI']):
4512
				return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME']));
4513
		}
4514
	}
4515
4516
	/**
4517
	 * Get path to requested .scss file
4518
	 *
4519
	 * @return string
4520
	 */
4521
	protected function findInput() {
4522
		if (($input = $this->inputName())
4523
			&& strpos($input, '..') == false
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing strpos($input, '..') of type integer to the boolean false. If you are specifically checking for 0, consider using something more explicit like === 0 instead.
Loading history...
4524
			&& substr($input, -5) == '.scss'
4525
		) {
4526
			$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...
4527
4528
			if (is_file($name) && is_readable($name)) {
4529
				return $name;
4530
			}
4531
		}
4532
4533
		return false;
4534
	}
4535
4536
	/**
4537
	 * Get path to cached .css file
4538
	 *
4539
	 * @return string
4540
	 */
4541
	protected function cacheName($fname) {
4542
		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...
4543
	}
4544
4545
	/**
4546
	 * Get path to meta data
4547
	 *
4548
	 * @return string
4549
	 */
4550
	protected function metadataName($out) {
4551
		return $out . '.meta';
4552
	}
4553
4554
	/**
4555
	 * Determine whether .scss file needs to be re-compiled.
4556
	 *
4557
	 * @param string $in   Input path
4558
	 * @param string $out  Output path
4559
	 * @param string $etag ETag
4560
	 *
4561
	 * @return boolean True if compile required.
4562
	 */
4563
	protected function needsCompile($in, $out, &$etag) {
4564
		if ( ! is_file($out)) {
4565
			return true;
4566
		}
4567
4568
		$mtime = filemtime($out);
4569
4570
		if (filemtime($in) > $mtime) {
4571
			return true;
4572
		}
4573
4574
		$metadataName = $this->metadataName($out);
4575
4576
		if (is_readable($metadataName)) {
4577
			$metadata = unserialize(file_get_contents($metadataName));
4578
4579
			if ($metadata['etag'] == $etag) {
4580
				return false;
4581
			}
4582
4583
			foreach ($metadata['imports'] as $import) {
4584
				if (filemtime($import) > $mtime) {
4585
					return true;
4586
				}
4587
			}
4588
4589
			$etag = $metadata['etag'];
4590
4591
			return false;
4592
		}
4593
4594
		return true;
4595
	}
4596
4597
	/**
4598
	 * Get If-Modified-Since header from client request
4599
	 *
4600
	 * @return string|null
4601
	 */
4602
	protected function getIfModifiedSinceHeader()
4603
	{
4604
		$modifiedSince = null;
4605
4606
		if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
4607
			$modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
4608
4609
			if (false != ($semicolonPos = strpos($modifiedSince, ';'))) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $semicolonPos = strpos($modifiedSince, ';') of type integer to the boolean false. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
4610
				$modifiedSince = substr($modifiedSince, 0, $semicolonPos);
4611
			}
4612
		}
4613
4614
		return $modifiedSince;
4615
	}
4616
4617
	/**
4618
	 * Get If-None-Match header from client request
4619
	 *
4620
	 * @return string|null
4621
	 */
4622
	protected function getIfNoneMatchHeader()
4623
	{
4624
		$noneMatch = null;
4625
4626
		if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
4627
			$noneMatch = $_SERVER['HTTP_IF_NONE_MATCH'];
4628
		}
4629
4630
		return $noneMatch;
4631
	}
4632
4633
	/**
4634
	 * Compile .scss file
4635
	 *
4636
	 * @param string $in  Input path (.scss)
4637
	 * @param string $out Output path (.css)
4638
	 *
4639
	 * @return array
4640
	 */
4641
	protected function compile($in, $out)
4642
	{
4643
		$start   = microtime(true);
4644
		$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...
4645
		$elapsed = round((microtime(true) - $start), 4);
4646
4647
		$v    = titanscssc::$VERSION;
4648
		$t    = @date('r');
4649
		$css  = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css;
4650
		$etag = md5($css);
4651
4652
		file_put_contents($out, $css);
4653
		file_put_contents(
4654
			$this->metadataName($out),
4655
			serialize(array(
4656
				'etag'    => $etag,
4657
				'imports' => $this->scss->getParsedFiles(),
4658
			))
4659
		);
4660
4661
		return array($css, $etag);
4662
	}
4663
4664
	/**
4665
	 * Compile requested scss and serve css.  Outputs HTTP response.
4666
	 *
4667
	 * @param string $salt Prefix a string to the filename for creating the cache name hash
4668
	 */
4669
	public function serve($salt = '') {
4670
		$protocol = isset($_SERVER['SERVER_PROTOCOL'])
4671
			? $_SERVER['SERVER_PROTOCOL']
4672
			: 'HTTP/1.0';
4673
4674
		if ($input = $this->findInput()) {
4675
			$output = $this->cacheName($salt . $input);
4676
			$etag = $noneMatch = trim($this->getIfNoneMatchHeader(), '"');
4677
4678
			if ($this->needsCompile($input, $output, $etag)) {
4679
				try {
4680
					list($css, $etag) = $this->compile($input, $output);
4681
4682
					$lastModified = gmdate('D, d M Y H:i:s', filemtime($output)) . ' GMT';
4683
4684
					header('Last-Modified: ' . $lastModified);
4685
					header('Content-type: text/css');
4686
					header('ETag: "' . $etag . '"');
4687
4688
					echo $css;
4689
4690
					return;
4691
				} catch (Exception $e) {
4692
					header($protocol . ' 500 Internal Server Error');
4693
					header('Content-type: text/plain');
4694
4695
					echo 'Parse error: ' . $e->getMessage() . "\n";
4696
				}
4697
			}
4698
4699
			header('X-SCSS-Cache: true');
4700
			header('Content-type: text/css');
4701
			header('ETag: "' . $etag . '"');
4702
4703
			if ($etag == $noneMatch) {
4704
				header($protocol . ' 304 Not Modified');
4705
4706
				return;
4707
			}
4708
4709
			$modifiedSince = $this->getIfModifiedSinceHeader();
4710
			$mtime = filemtime($output);
4711
4712
			if (@strtotime($modifiedSince) == $mtime) {
4713
				header($protocol . ' 304 Not Modified');
4714
4715
				return;
4716
			}
4717
4718
			$lastModified  = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
4719
			header('Last-Modified: ' . $lastModified);
4720
4721
			echo file_get_contents($output);
4722
4723
			return;
4724
		}
4725
4726
		header($protocol . ' 404 Not Found');
4727
		header('Content-type: text/plain');
4728
4729
		$v = titanscssc::$VERSION;
4730
		echo "/* INPUT NOT FOUND scss $v */\n";
4731
	}
4732
4733
	/**
4734
	 * Constructor
4735
	 *
4736
	 * @param string      $dir      Root directory to .scss files
4737
	 * @param string      $cacheDir Cache directory
4738
	 * @param \scssc|null $scss     SCSS compiler instance
4739
	 */
4740
	public function __construct($dir, $cacheDir=null, $scss=null) {
4741
		$this->dir = $dir;
4742
4743
		if (!isset($cacheDir)) {
4744
			$cacheDir = $this->join($dir, 'titanscss_cache');
4745
		}
4746
4747
		$this->cacheDir = $cacheDir;
4748
		if (!is_dir($this->cacheDir)) mkdir($this->cacheDir, 0755, true);
4749
4750
		if (!isset($scss)) {
4751
			$scss = new titanscssc();
4752
			$scss->setImportPaths($this->dir);
4753
		}
4754
		$this->scss = $scss;
4755
	}
4756
4757
	/**
4758
	 * Helper method to serve compiled scss
4759
	 *
4760
	 * @param string $path Root path
4761
	 */
4762
	static public function serveFrom($path) {
0 ignored issues
show
Coding Style introduced by
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
4763
		$server = new self($path);
4764
		$server->serve();
4765
	}
4766
}
4767