Completed
Push — master ( 7473b0...2b6e28 )
by
unknown
10:12
created

Less_Parser   F

Complexity

Total Complexity 499

Size/Duplication

Total Lines 2590
Duplicated Lines 1.51 %

Coupling/Cohesion

Components 1
Dependencies 11
Metric Value
wmc 499
lcom 1
cbo 11
dl 39
loc 2590
rs 3.9999

112 Methods

Rating   Name   Duplication   Size   Complexity  
A SetOption() 0 18 4
A registerFunction() 0 3 1
A unregisterFunction() 0 4 2
A PreVisitors() 0 10 4
A SetInput() 0 19 4
A UnsetInput() 0 4 1
A AddParsedFile() 0 3 1
A AllParsedFiles() 0 3 1
A FileParsed() 0 3 1
A save() 0 3 1
A restore() 0 3 1
A forget() 0 3 1
A isWhitespace() 0 3 1
A MatchFuncs() 0 12 4
A PeekReg() 0 3 1
A PeekChar() 0 4 2
A parseComment() 0 17 4
A parseEntitiesArguments() 0 16 4
A parseEntitiesLiteral() 0 3 1
A parseEntitiesAssignment() 0 16 4
A parseEntitiesVariableCurly() 0 7 4
A parseEntity() 0 4 1
A parseEnd() 0 3 2
A parseAlpha() 0 19 4
A parseLessSelector() 0 3 1
A parseTag() 0 3 2
A serializeVars() 0 9 4
A is_method() 0 3 2
A ObjCache() 0 3 1
A ArgCache() 0 3 1
A ArgString() 0 19 4
A Error() 0 3 1
A WinPath() 0 3 1
A CacheEnabled() 0 3 3
A __construct() 0 12 2
A Reset() 0 16 2
A SetOptions() 0 5 2
B getCss() 0 40 3
D PostVisitors() 0 30 9
B parse() 0 24 3
C parseFile() 0 35 7
A ModifyVars() 0 7 1
B SetFileInfo() 0 36 5
B SetCacheDir() 0 20 5
B SetImportDirs() 0 20 5
A _parse() 0 13 3
D GetRules() 0 83 17
A CacheFile() 0 17 3
C match() 0 33 7
A MatchChar() 0 6 3
A MatchReg() 0 7 2
B skipWhitespace() 0 12 6
A expect() 8 8 3
A expectChar() 8 8 3
C parsePrimary() 0 31 7
A parseComments() 0 14 3
C parseEntitiesQuoted() 0 28 7
A parseEntitiesKeyword() 0 13 3
A FromKeyword() 0 12 3
C parseEntitiesCall() 0 34 7
B parseEntitiesUrl() 0 22 6
A parseEntitiesVariable() 0 6 3
A parseEntitiesColor() 0 5 3
B parseEntitiesDimension() 0 18 7
A parseUnicodeDescriptor() 0 6 2
B parseEntitiesJavascript() 0 18 5
A parseVariable() 0 5 3
A parseRulesetCall() 0 6 3
D parseExtend() 0 35 9
B parseMixinCall() 0 32 6
A parseMixinCallElements() 0 16 3
F parseMixinArgs() 0 136 34
C parseMixinDefinition() 0 49 9
B parseElement() 0 25 6
C parseCombinator() 0 21 10
D parseSelector() 0 38 17
B parseAttribute() 0 22 4
A parseBlock() 0 8 3
A parseBlockRuleset() 0 9 2
A parseDetachedRuleset() 0 6 2
D parseRuleset() 0 39 9
B parseNameValue() 0 23 4
C parseRule() 0 62 17
A parseAnonymousValue() 0 7 2
B parseImport() 0 25 5
C parseImportOptions() 0 29 7
A parseImportOption() 0 6 2
D parseMediaFeature() 0 28 9
B parseMediaFeatures() 0 19 7
A parseMedia() 0 10 3
F parseDirective() 0 105 24
B parseValue() 0 17 5
A parseImportant() 0 5 3
A parseSub() 0 10 3
C parseMultiplication() 0 32 7
C parseAddition() 0 33 7
B parseConditions() 0 18 6
B parseCondition() 9 26 6
B parseOperand() 0 21 5
B parseExpression() 0 21 6
A parseProperty() 0 6 2
C parseRuleProperty() 0 31 8
A rulePropertyMatch() 0 10 2
A round() 0 13 2
A NewObj0() 0 7 2
A NewObj1() 0 7 2
A NewObj2() 0 7 2
A NewObj3() 0 7 2
A NewObj4() 0 7 2
A NewObj5() 0 7 2
A NewObj6() 7 7 2
A NewObj7() 7 7 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

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

Common duplication problems, and corresponding solutions are:

Complex Class

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

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

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

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

1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 12 and the first side effect is on line 3.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
require_once( dirname(__FILE__).'/Cache.php');
4
5
/**
6
 * Class for parsing and compiling less files into css
7
 *
8
 * @package Less
9
 * @subpackage parser
10
 *
11
 */
12
class Less_Parser{
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
13
14
15
	/**
16
	 * Default parser options
17
	 */
18
	public static $default_options = array(
19
		'compress'				=> false,			// option - whether to compress
20
		'strictUnits'			=> false,			// whether units need to evaluate correctly
21
		'strictMath'			=> false,			// whether math has to be within parenthesis
22
		'relativeUrls'			=> true,			// option - whether to adjust URL's to be relative
23
		'urlArgs'				=> array(),			// whether to add args into url tokens
24
		'numPrecision'			=> 8,
25
26
		'import_dirs'			=> array(),
27
		'import_callback'		=> null,
28
		'cache_dir'				=> null,
29
		'cache_method'			=> 'php', 			// false, 'serialize', 'php', 'var_export', 'callback';
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% 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...
30
		'cache_callback_get'	=> null,
31
		'cache_callback_set'	=> null,
32
33
		'sourceMap'				=> false,			// whether to output a source map
34
		'sourceMapBasepath'		=> null,
35
		'sourceMapWriteTo'		=> null,
36
		'sourceMapURL'			=> null,
37
38
		'plugins'				=> array(),
39
40
	);
41
42
	public static $options = array();
43
44
45
	private $input;					// Less input string
46
	private $input_len;				// input string length
47
	private $pos;					// current index in `input`
48
	private $saveStack = array();	// holds state for backtracking
49
	private $furthest;
50
51
	/**
52
	 * @var Less_Environment
53
	 */
54
	private $env;
55
56
	private $rules = array();
57
58
	private static $imports = array();
59
60
	public static $has_extends = false;
61
62
	public static $next_id = 0;
63
64
	/**
65
	 * Filename to contents of all parsed the files
66
	 *
67
	 * @var array
68
	 */
69
	public static $contentsMap = array();
70
71
72
	/**
73
	 * @param Less_Environment|array|null $env
74
	 */
75
	public function __construct( $env = null ){
76
77
		// Top parser on an import tree must be sure there is one "env"
78
		// which will then be passed around by reference.
79
		if( $env instanceof Less_Environment ){
80
			$this->env = $env;
81
		}else{
82
			$this->SetOptions(Less_Parser::$default_options);
83
			$this->Reset( $env );
84
		}
85
86
	}
87
88
89
	/**
90
	 * Reset the parser state completely
91
	 *
92
	 */
93
	public function Reset( $options = null ){
94
		$this->rules = array();
95
		self::$imports = array();
96
		self::$has_extends = false;
97
		self::$imports = array();
98
		self::$contentsMap = array();
99
100
		$this->env = new Less_Environment($options);
0 ignored issues
show
Unused Code introduced by
The call to Less_Environment::__construct() has too many arguments starting with $options.

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...
101
		$this->env->Init();
102
103
		//set new options
104
		if( is_array($options) ){
105
			$this->SetOptions(Less_Parser::$default_options);
106
			$this->SetOptions($options);
107
		}
108
	}
109
110
	/**
111
	 * Set one or more compiler options
112
	 *  options: import_dirs, cache_dir, cache_method
113
	 *
114
	 */
115
	public function SetOptions( $options ){
116
		foreach($options as $option => $value){
117
			$this->SetOption($option,$value);
118
		}
119
	}
120
121
	/**
122
	 * Set one compiler option
123
	 *
124
	 */
125
	public function SetOption($option,$value){
126
127
		switch($option){
128
129
			case 'import_dirs':
130
				$this->SetImportDirs($value);
131
			return;
132
133
			case 'cache_dir':
134
				if( is_string($value) ){
135
					Less_Cache::SetCacheDir($value);
136
					Less_Cache::CheckCacheDir();
137
				}
138
			return;
139
		}
140
141
		Less_Parser::$options[$option] = $value;
142
	}
143
144
	/**
145
	 * Registers a new custom function
146
	 *
147
	 * @param  string   $name     function name
148
	 * @param  callable $callback callback
149
	 */
150
	public function registerFunction($name, $callback) {
151
		$this->env->functions[$name] = $callback;
152
	}
153
154
	/**
155
	 * Removed an already registered function
156
	 *
157
	 * @param  string $name function name
158
	 */
159
	public function unregisterFunction($name) {
160
		if( isset($this->env->functions[$name]) )
161
			unset($this->env->functions[$name]);
162
	}
163
164
165
	/**
166
	 * Get the current css buffer
167
	 *
168
	 * @return string
169
	 */
170
	public function getCss(){
171
172
		$precision = ini_get('precision');
173
		@ini_set('precision',16);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
174
		$locale = setlocale(LC_NUMERIC, 0);
175
		setlocale(LC_NUMERIC, "C");
176
177
 		$root = new Less_Tree_Ruleset(array(), $this->rules );
178
		$root->root = true;
179
		$root->firstRoot = true;
180
181
182
		$this->PreVisitors($root);
183
184
		self::$has_extends = false;
185
		$evaldRoot = $root->compile($this->env);
186
187
188
189
		$this->PostVisitors($evaldRoot);
190
191
		if( Less_Parser::$options['sourceMap'] ){
192
			$generator = new Less_SourceMap_Generator($evaldRoot, Less_Parser::$contentsMap, Less_Parser::$options );
193
			// will also save file
194
			// FIXME: should happen somewhere else?
195
			$css = $generator->generateCSS();
196
		}else{
197
			$css = $evaldRoot->toCSS();
198
		}
199
200
		if( Less_Parser::$options['compress'] ){
201
			$css = preg_replace('/(^(\s)+)|((\s)+$)/', '', $css);
202
		}
203
204
		//reset php settings
205
		@ini_set('precision',$precision);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
206
		setlocale(LC_NUMERIC, $locale);
207
208
		return $css;
209
	}
210
211
	/**
212
	 * Run pre-compile visitors
213
	 *
214
	 */
215
	private function PreVisitors($root){
216
217
		if( Less_Parser::$options['plugins'] ){
218
			foreach(Less_Parser::$options['plugins'] as $plugin){
219
				if( !empty($plugin->isPreEvalVisitor) ){
220
					$plugin->run($root);
221
				}
222
			}
223
		}
224
	}
225
226
227
	/**
228
	 * Run post-compile visitors
229
	 *
230
	 */
231
	private function PostVisitors($evaldRoot){
232
233
		$visitors = array();
234
		$visitors[] = new Less_Visitor_joinSelector();
235
		if( self::$has_extends ){
236
			$visitors[] = new Less_Visitor_processExtends();
237
		}
238
		$visitors[] = new Less_Visitor_toCSS();
239
240
241
		if( Less_Parser::$options['plugins'] ){
242
			foreach(Less_Parser::$options['plugins'] as $plugin){
243
				if( property_exists($plugin,'isPreEvalVisitor') && $plugin->isPreEvalVisitor ){
244
					continue;
245
				}
246
247
				if( property_exists($plugin,'isPreVisitor') && $plugin->isPreVisitor ){
248
					array_unshift( $visitors, $plugin);
249
				}else{
250
					$visitors[] = $plugin;
251
				}
252
			}
253
		}
254
255
256
		for($i = 0; $i < count($visitors); $i++ ){
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
257
			$visitors[$i]->run($evaldRoot);
258
		}
259
260
	}
261
262
263
	/**
264
	 * Parse a Less string into css
265
	 *
266
	 * @param string $str The string to convert
267
	 * @param string $uri_root The url of the file
0 ignored issues
show
Bug introduced by
There is no parameter named $uri_root. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

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

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

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

Loading history...
268
	 * @return Less_Tree_Ruleset|Less_Parser
269
	 */
270
	public function parse( $str, $file_uri = null ){
271
272
		if( !$file_uri ){
273
			$uri_root = '';
274
			$filename = 'anonymous-file-'.Less_Parser::$next_id++.'.less';
275
		}else{
276
			$file_uri = self::WinPath($file_uri);
277
			$filename = basename($file_uri);
278
			$uri_root = dirname($file_uri);
279
		}
280
281
		$previousFileInfo = $this->env->currentFileInfo;
282
		$uri_root = self::WinPath($uri_root);
283
		$this->SetFileInfo($filename, $uri_root);
284
285
		$this->input = $str;
286
		$this->_parse();
287
288
		if( $previousFileInfo ){
289
			$this->env->currentFileInfo = $previousFileInfo;
290
		}
291
292
		return $this;
293
	}
294
295
296
	/**
297
	 * Parse a Less string from a given file
298
	 *
299
	 * @throws Less_Exception_Parser
300
	 * @param string $filename The file to parse
301
	 * @param string $uri_root The url of the file
302
	 * @param bool $returnRoot Indicates whether the return value should be a css string a root node
303
	 * @return Less_Tree_Ruleset|Less_Parser
304
	 */
305
	public function parseFile( $filename, $uri_root = '', $returnRoot = false){
306
307
		if( !file_exists($filename) ){
308
			$this->Error(sprintf('File `%s` not found.', $filename));
309
		}
310
311
312
		// fix uri_root?
313
		// Instead of The mixture of file path for the first argument and directory path for the second argument has bee
314
		if( !$returnRoot && !empty($uri_root) && basename($uri_root) == basename($filename) ){
315
			$uri_root = dirname($uri_root);
316
		}
317
318
319
		$previousFileInfo = $this->env->currentFileInfo;
320
		$filename = self::WinPath($filename);
321
		$uri_root = self::WinPath($uri_root);
322
		$this->SetFileInfo($filename, $uri_root);
323
324
		self::AddParsedFile($filename);
325
326
		if( $returnRoot ){
327
			$rules = $this->GetRules( $filename );
328
			$return = new Less_Tree_Ruleset(array(), $rules );
329
		}else{
330
			$this->_parse( $filename );
331
			$return = $this;
332
		}
333
334
		if( $previousFileInfo ){
335
			$this->env->currentFileInfo = $previousFileInfo;
336
		}
337
338
		return $return;
339
	}
340
341
342
	/**
343
	 * Allows a user to set variables values
344
	 * @param array $vars
345
	 * @return Less_Parser
346
	 */
347
	public function ModifyVars( $vars ){
348
349
		$this->input = Less_Parser::serializeVars( $vars );
350
		$this->_parse();
351
352
		return $this;
353
	}
354
355
356
	/**
357
	 * @param string $filename
358
	 */
359
	public function SetFileInfo( $filename, $uri_root = ''){
360
361
		$filename = Less_Environment::normalizePath($filename);
362
		$dirname = preg_replace('/[^\/\\\\]*$/','',$filename);
363
364
		if( !empty($uri_root) ){
365
			$uri_root = rtrim($uri_root,'/').'/';
366
		}
367
368
		$currentFileInfo = array();
369
370
		//entry info
371
		if( isset($this->env->currentFileInfo) ){
372
			$currentFileInfo['entryPath'] = $this->env->currentFileInfo['entryPath'];
373
			$currentFileInfo['entryUri'] = $this->env->currentFileInfo['entryUri'];
374
			$currentFileInfo['rootpath'] = $this->env->currentFileInfo['rootpath'];
375
376
		}else{
377
			$currentFileInfo['entryPath'] = $dirname;
378
			$currentFileInfo['entryUri'] = $uri_root;
379
			$currentFileInfo['rootpath'] = $dirname;
380
		}
381
382
		$currentFileInfo['currentDirectory'] = $dirname;
383
		$currentFileInfo['currentUri'] = $uri_root.basename($filename);
384
		$currentFileInfo['filename'] = $filename;
385
		$currentFileInfo['uri_root'] = $uri_root;
386
387
388
		//inherit reference
389
		if( isset($this->env->currentFileInfo['reference']) && $this->env->currentFileInfo['reference'] ){
390
			$currentFileInfo['reference'] = true;
391
		}
392
393
		$this->env->currentFileInfo = $currentFileInfo;
394
	}
395
396
397
	/**
398
	 * @deprecated 1.5.1.2
399
	 *
400
	 */
401
	public function SetCacheDir( $dir ){
402
403
		if( !file_exists($dir) ){
404
			if( mkdir($dir) ){
405
				return true;
406
			}
407
			throw new Less_Exception_Parser('Less.php cache directory couldn\'t be created: '.$dir);
408
409
		}elseif( !is_dir($dir) ){
410
			throw new Less_Exception_Parser('Less.php cache directory doesn\'t exist: '.$dir);
411
412
		}elseif( !is_writable($dir) ){
413
			throw new Less_Exception_Parser('Less.php cache directory isn\'t writable: '.$dir);
414
415
		}else{
416
			$dir = self::WinPath($dir);
417
			Less_Cache::$cache_dir = rtrim($dir,'/').'/';
0 ignored issues
show
Documentation Bug introduced by
The property $cache_dir was declared of type boolean, but rtrim($dir, '/') . '/' is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
418
			return true;
419
		}
420
	}
421
422
423
	/**
424
	 * Set a list of directories or callbacks the parser should use for determining import paths
425
	 *
426
	 * @param array $dirs
427
	 */
428
	public function SetImportDirs( $dirs ){
429
		Less_Parser::$options['import_dirs'] = array();
430
431
		foreach($dirs as $path => $uri_root){
432
433
			$path = self::WinPath($path);
434
			if( !empty($path) ){
435
				$path = rtrim($path,'/').'/';
436
			}
437
438
			if ( !is_callable($uri_root) ){
439
				$uri_root = self::WinPath($uri_root);
440
				if( !empty($uri_root) ){
441
					$uri_root = rtrim($uri_root,'/').'/';
442
				}
443
			}
444
445
			Less_Parser::$options['import_dirs'][$path] = $uri_root;
446
		}
447
	}
448
449
	/**
450
	 * @param string $file_path
451
	 */
452
	private function _parse( $file_path = null ){
453
		if (ini_get("mbstring.func_overload")) {
454
			$mb_internal_encoding = ini_get("mbstring.internal_encoding");
455
			@ini_set("mbstring.internal_encoding", "ascii");
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
456
		}
457
458
		$this->rules = array_merge($this->rules, $this->GetRules( $file_path ));
459
460
		//reset php settings
461
		if (isset($mb_internal_encoding)) {
462
			@ini_set("mbstring.internal_encoding", $mb_internal_encoding);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

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

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

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
463
		}
464
	}
465
466
467
	/**
468
	 * Return the results of parsePrimary for $file_path
469
	 * Use cache and save cached results if possible
470
	 *
471
	 * @param string|null $file_path
472
	 */
473
	private function GetRules( $file_path ){
474
475
		$this->SetInput($file_path);
476
477
		$cache_file = $this->CacheFile( $file_path );
478
		if( $cache_file ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $cache_file of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
479
			if( Less_Parser::$options['cache_method'] == 'callback' ){
480
				if( is_callable(Less_Parser::$options['cache_callback_get']) ){
481
					$cache = call_user_func_array(
482
						Less_Parser::$options['cache_callback_get'],
483
						array($this, $file_path, $cache_file)
484
					);
485
486
					if( $cache ){
487
						$this->UnsetInput();
488
						return $cache;
489
					}
490
				}
491
492
			}elseif( file_exists($cache_file) ){
493
				switch(Less_Parser::$options['cache_method']){
494
495
					// Using serialize
496
					// Faster but uses more memory
497
					case 'serialize':
498
						$cache = unserialize(file_get_contents($cache_file));
499
						if( $cache ){
500
							touch($cache_file);
501
							$this->UnsetInput();
502
							return $cache;
503
						}
504
					break;
505
506
507
					// Using generated php code
508
					case 'var_export':
509
					case 'php':
510
					$this->UnsetInput();
511
					return include($cache_file);
512
				}
513
			}
514
		}
515
516
		$rules = $this->parsePrimary();
517
518
		if( $this->pos < $this->input_len ){
519
			throw new Less_Exception_Chunk($this->input, null, $this->furthest, $this->env->currentFileInfo);
0 ignored issues
show
Documentation introduced by
$this->env->currentFileInfo is of type array<string,?>, but the function expects a object<Less_FileInfo>|string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
520
		}
521
522
		$this->UnsetInput();
523
524
525
		//save the cache
526
		if( $cache_file ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $cache_file of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
527
			if( Less_Parser::$options['cache_method'] == 'callback' ){
528
				if( is_callable(Less_Parser::$options['cache_callback_set']) ){
529
					call_user_func_array(
530
						Less_Parser::$options['cache_callback_set'],
531
						array($this, $file_path, $cache_file, $rules)
532
					);
533
				}
534
535
			}else{
536
				//msg('write cache file');
537
				switch(Less_Parser::$options['cache_method']){
538
					case 'serialize':
539
						file_put_contents( $cache_file, serialize($rules) );
540
					break;
541
					case 'php':
542
						file_put_contents( $cache_file, '<?php return '.self::ArgString($rules).'; ?>' );
543
					break;
544
					case 'var_export':
545
						//Requires __set_state()
546
						file_put_contents( $cache_file, '<?php return '.var_export($rules,true).'; ?>' );
547
					break;
548
				}
549
550
				Less_Cache::CleanCache();
551
			}
552
		}
553
554
		return $rules;
555
	}
556
557
558
	/**
559
	 * Set up the input buffer
560
	 *
561
	 */
562
	public function SetInput( $file_path ){
563
564
		if( $file_path ){
565
			$this->input = file_get_contents( $file_path );
566
		}
567
568
		$this->pos = $this->furthest = 0;
569
570
		// Remove potential UTF Byte Order Mark
571
		$this->input = preg_replace('/\\G\xEF\xBB\xBF/', '', $this->input);
572
		$this->input_len = strlen($this->input);
573
574
575
		if( Less_Parser::$options['sourceMap'] && $this->env->currentFileInfo ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->env->currentFileInfo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
576
			$uri = $this->env->currentFileInfo['currentUri'];
577
			Less_Parser::$contentsMap[$uri] = $this->input;
578
		}
579
580
	}
581
582
583
	/**
584
	 * Free up some memory
585
	 *
586
	 */
587
	public function UnsetInput(){
588
		unset($this->input, $this->pos, $this->input_len, $this->furthest);
589
		$this->saveStack = array();
590
	}
591
592
593
	public function CacheFile( $file_path ){
594
595
		if( $file_path && $this->CacheEnabled() ){
596
597
			$env = get_object_vars($this->env);
598
			unset($env['frames']);
599
600
			$parts = array();
601
			$parts[] = $file_path;
602
			$parts[] = filesize( $file_path );
603
			$parts[] = filemtime( $file_path );
604
			$parts[] = $env;
605
			$parts[] = Less_Version::cache_version;
606
			$parts[] = Less_Parser::$options['cache_method'];
607
			return Less_Cache::$cache_dir.'lessphp_'.base_convert( sha1(json_encode($parts) ), 16, 36).'.lesscache';
608
		}
609
	}
610
611
612
	static function AddParsedFile($file){
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...
613
		self::$imports[] = $file;
614
	}
615
616
	static function AllParsedFiles(){
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...
617
		return self::$imports;
618
	}
619
620
	/**
621
	 * @param string $file
622
	 */
623
	static function FileParsed($file){
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...
624
		return in_array($file,self::$imports);
625
	}
626
627
628
	function save() {
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...
629
		$this->saveStack[] = $this->pos;
630
	}
631
632
	private function restore() {
633
		$this->pos = array_pop($this->saveStack);
634
	}
635
636
	private function forget(){
637
		array_pop($this->saveStack);
638
	}
639
640
641
	private function isWhitespace($offset = 0) {
642
		return preg_match('/\s/',$this->input[ $this->pos + $offset]);
643
	}
644
645
	/**
646
	 * Parse from a token, regexp or string, and move forward if match
647
	 *
648
	 * @param array $toks
649
	 * @return array
650
	 */
651
	private function match($toks){
652
653
		// The match is confirmed, add the match length to `this::pos`,
654
		// and consume any extra white-space characters (' ' || '\n')
655
		// which come after that. The reason for this is that LeSS's
656
		// grammar is mostly white-space insensitive.
657
		//
658
659
		foreach($toks as $tok){
660
661
			$char = $tok[0];
662
663
			if( $char === '/' ){
664
				$match = $this->MatchReg($tok);
665
666
				if( $match ){
667
					return count($match) === 1 ? $match[0] : $match;
668
				}
669
670
			}elseif( $char === '#' ){
671
				$match = $this->MatchChar($tok[1]);
672
673
			}else{
674
				// Non-terminal, match using a function call
675
				$match = $this->$tok();
676
677
			}
678
679
			if( $match ){
680
				return $match;
681
			}
682
		}
683
	}
684
685
	/**
686
	 * @param string[] $toks
687
	 *
688
	 * @return string
689
	 */
690
	private function MatchFuncs($toks){
691
692
		if( $this->pos < $this->input_len ){
693
			foreach($toks as $tok){
694
				$match = $this->$tok();
695
				if( $match ){
696
					return $match;
697
				}
698
			}
699
		}
700
701
	}
702
703
	// Match a single character in the input,
704
	private function MatchChar($tok){
705
		if( ($this->pos < $this->input_len) && ($this->input[$this->pos] === $tok) ){
706
			$this->skipWhitespace(1);
707
			return $tok;
708
		}
709
	}
710
711
	// Match a regexp from the current start point
712
	private function MatchReg($tok){
713
714
		if( preg_match($tok, $this->input, $match, 0, $this->pos) ){
715
			$this->skipWhitespace(strlen($match[0]));
716
			return $match;
717
		}
718
	}
719
720
721
	/**
722
	 * Same as match(), but don't change the state of the parser,
723
	 * just return the match.
724
	 *
725
	 * @param string $tok
726
	 * @return integer
727
	 */
728
	public function PeekReg($tok){
729
		return preg_match($tok, $this->input, $match, 0, $this->pos);
730
	}
731
732
	/**
733
	 * @param string $tok
734
	 */
735
	public function PeekChar($tok){
736
		//return ($this->input[$this->pos] === $tok );
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% 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...
737
		return ($this->pos < $this->input_len) && ($this->input[$this->pos] === $tok );
738
	}
739
740
741
	/**
742
	 * @param integer $length
743
	 */
744
	public function skipWhitespace($length){
745
746
		$this->pos += $length;
747
748
		for(; $this->pos < $this->input_len; $this->pos++ ){
749
			$c = $this->input[$this->pos];
750
751
			if( ($c !== "\n") && ($c !== "\r") && ($c !== "\t") && ($c !== ' ') ){
752
				break;
753
			}
754
		}
755
	}
756
757
758
	/**
759
	 * @param string $tok
760
	 * @param string|null $msg
761
	 */
762 View Code Duplication
	public function expect($tok, $msg = NULL) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
763
		$result = $this->match( array($tok) );
764
		if (!$result) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
765
			$this->Error( $msg	? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg );
766
		} else {
767
			return $result;
768
		}
769
	}
770
771
	/**
772
	 * @param string $tok
773
	 */
774 View Code Duplication
	public function expectChar($tok, $msg = null ){
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
775
		$result = $this->MatchChar($tok);
776
		if( !$result ){
777
			$this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg );
778
		}else{
779
			return $result;
780
		}
781
	}
782
783
	//
784
	// Here in, the parsing rules/functions
785
	//
786
	// The basic structure of the syntax tree generated is as follows:
787
	//
788
	//   Ruleset ->  Rule -> Value -> Expression -> Entity
789
	//
790
	// Here's some LESS code:
791
	//
792
	//	.class {
793
	//	  color: #fff;
794
	//	  border: 1px solid #000;
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% 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...
795
	//	  width: @w + 4px;
796
	//	  > .child {...}
797
	//	}
798
	//
799
	// And here's what the parse tree might look like:
800
	//
801
	//	 Ruleset (Selector '.class', [
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% 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...
802
	//		 Rule ("color",  Value ([Expression [Color #fff]]))
0 ignored issues
show
Unused Code Comprehensibility introduced by
44% 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...
803
	//		 Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]]))
0 ignored issues
show
Unused Code Comprehensibility introduced by
49% 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...
804
	//		 Rule ("width",  Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]]))
0 ignored issues
show
Unused Code Comprehensibility introduced by
52% 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...
805
	//		 Ruleset (Selector [Element '>', '.child'], [...])
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% 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...
806
	//	 ])
807
	//
808
	//  In general, most rules will try to parse a token with the `$()` function, and if the return
809
	//  value is truly, will return a new node, of the relevant type. Sometimes, we need to check
810
	//  first, before parsing, that's when we use `peek()`.
811
	//
812
813
	//
814
	// The `primary` rule is the *entry* and *exit* point of the parser.
815
	// The rules here can appear at any level of the parse tree.
816
	//
817
	// The recursive nature of the grammar is an interplay between the `block`
818
	// rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,
819
	// as represented by this simplified grammar:
820
	//
821
	//	 primary  →  (ruleset | rule)+
822
	//	 ruleset  →  selector+ block
823
	//	 block	→  '{' primary '}'
824
	//
825
	// Only at one point is the primary rule not called from the
826
	// block rule: at the root level.
827
	//
828
	private function parsePrimary(){
829
		$root = array();
830
831
		while( true ){
832
833
			if( $this->pos >= $this->input_len ){
834
				break;
835
			}
836
837
			$node = $this->parseExtend(true);
838
			if( $node ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $node of type object[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
839
				$root = array_merge($root,$node);
840
				continue;
841
			}
842
843
			//$node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseDirective'));
0 ignored issues
show
Unused Code Comprehensibility introduced by
65% 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...
844
			$node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseNameValue', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseRulesetCall', 'parseDirective'));
845
846
			if( $node ){
847
				$root[] = $node;
848
			}elseif( !$this->MatchReg('/\\G[\s\n;]+/') ){
849
				break;
850
			}
851
852
            if( $this->PeekChar('}') ){
853
				break;
854
			}
855
		}
856
857
		return $root;
858
	}
859
860
861
862
	// We create a Comment node for CSS comments `/* */`,
863
	// but keep the LeSS comments `//` silent, by just skipping
864
	// over them.
865
	private function parseComment(){
866
867
		if( $this->input[$this->pos] !== '/' ){
868
			return;
869
		}
870
871
		if( $this->input[$this->pos+1] === '/' ){
872
			$match = $this->MatchReg('/\\G\/\/.*/');
873
			return $this->NewObj4('Less_Tree_Comment',array($match[0], true, $this->pos, $this->env->currentFileInfo));
874
		}
875
876
		//$comment = $this->MatchReg('/\\G\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/');
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% 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...
877
		$comment = $this->MatchReg('/\\G\/\*(?s).*?\*+\/\n?/');//not the same as less.js to prevent fatal errors
878
		if( $comment ){
879
			return $this->NewObj4('Less_Tree_Comment',array($comment[0], false, $this->pos, $this->env->currentFileInfo));
880
		}
881
	}
882
883
	private function parseComments(){
884
		$comments = array();
885
886
		while( $this->pos < $this->input_len ){
887
			$comment = $this->parseComment();
888
			if( !$comment ){
889
				break;
890
			}
891
892
			$comments[] = $comment;
893
		}
894
895
		return $comments;
896
	}
897
898
899
900
	//
901
	// A string, which supports escaping " and '
902
	//
903
	//	 "milky way" 'he\'s the one!'
904
	//
905
	private function parseEntitiesQuoted() {
906
		$j = $this->pos;
907
		$e = false;
908
		$index = $this->pos;
909
910
		if( $this->input[$this->pos] === '~' ){
911
			$j++;
912
			$e = true; // Escaped strings
913
		}
914
915
		if( $this->input[$j] != '"' && $this->input[$j] !== "'" ){
916
			return;
917
		}
918
919
		if ($e) {
920
			$this->MatchChar('~');
921
		}
922
923
                // Fix for #124: match escaped newlines
924
                //$str = $this->MatchReg('/\\G"((?:[^"\\\\\r\n]|\\\\.)*)"|\'((?:[^\'\\\\\r\n]|\\\\.)*)\'/');
925
		$str = $this->MatchReg('/\\G"((?:[^"\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)"|\'((?:[^\'\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)\'/');
926
927
		if( $str ){
928
			$result = $str[0][0] == '"' ? $str[1] : $str[2];
929
			return $this->NewObj5('Less_Tree_Quoted',array($str[0], $result, $e, $index, $this->env->currentFileInfo) );
930
		}
931
		return;
932
	}
933
934
935
	//
936
	// A catch-all word, such as:
937
	//
938
	//	 black border-collapse
939
	//
940
	private function parseEntitiesKeyword(){
941
942
		//$k = $this->MatchReg('/\\G[_A-Za-z-][_A-Za-z0-9-]*/');
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% 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...
943
		$k = $this->MatchReg('/\\G%|\\G[_A-Za-z-][_A-Za-z0-9-]*/');
944
		if( $k ){
945
			$k = $k[0];
946
			$color = $this->fromKeyword($k);
947
			if( $color ){
948
				return $color;
949
			}
950
			return $this->NewObj1('Less_Tree_Keyword',$k);
951
		}
952
	}
953
954
	// duplicate of Less_Tree_Color::FromKeyword
955
	private function FromKeyword( $keyword ){
956
		$keyword = strtolower($keyword);
957
958
		if( Less_Colors::hasOwnProperty($keyword) ){
959
			// detect named color
960
			return $this->NewObj1('Less_Tree_Color',substr(Less_Colors::color($keyword), 1));
961
		}
962
963
		if( $keyword === 'transparent' ){
964
			return $this->NewObj3('Less_Tree_Color', array( array(0, 0, 0), 0, true));
965
		}
966
	}
967
968
	//
969
	// A function call
970
	//
971
	//	 rgb(255, 0, 255)
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% 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...
972
	//
973
	// We also try to catch IE's `alpha()`, but let the `alpha` parser
974
	// deal with the details.
975
	//
976
	// The arguments are parsed with the `entities.arguments` parser.
977
	//
978
	private function parseEntitiesCall(){
979
		$index = $this->pos;
980
981
		if( !preg_match('/\\G([\w-]+|%|progid:[\w\.]+)\(/', $this->input, $name,0,$this->pos) ){
982
			return;
983
		}
984
		$name = $name[1];
985
		$nameLC = strtolower($name);
986
987
		if ($nameLC === 'url') {
988
			return null;
989
		}
990
991
		$this->pos += strlen($name);
992
993
		if( $nameLC === 'alpha' ){
994
			$alpha_ret = $this->parseAlpha();
995
			if( $alpha_ret ){
996
				return $alpha_ret;
997
			}
998
		}
999
1000
		$this->MatchChar('('); // Parse the '(' and consume whitespace.
1001
1002
		$args = $this->parseEntitiesArguments();
1003
1004
		if( !$this->MatchChar(')') ){
1005
			return;
1006
		}
1007
1008
		if ($name) {
1009
			return $this->NewObj4('Less_Tree_Call',array($name, $args, $index, $this->env->currentFileInfo) );
1010
		}
1011
	}
1012
1013
	/**
1014
	 * Parse a list of arguments
1015
	 *
1016
	 * @return array
1017
	 */
1018
	private function parseEntitiesArguments(){
1019
1020
		$args = array();
1021
		while( true ){
1022
			$arg = $this->MatchFuncs( array('parseEntitiesAssignment','parseExpression') );
1023
			if( !$arg ){
1024
				break;
1025
			}
1026
1027
			$args[] = $arg;
1028
			if( !$this->MatchChar(',') ){
1029
				break;
1030
			}
1031
		}
1032
		return $args;
1033
	}
1034
1035
	private function parseEntitiesLiteral(){
1036
		return $this->MatchFuncs( array('parseEntitiesDimension','parseEntitiesColor','parseEntitiesQuoted','parseUnicodeDescriptor') );
1037
	}
1038
1039
	// Assignments are argument entities for calls.
1040
	// They are present in ie filter properties as shown below.
1041
	//
1042
	//	 filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* )
1043
	//
1044
	private function parseEntitiesAssignment() {
1045
1046
		$key = $this->MatchReg('/\\G\w+(?=\s?=)/');
1047
		if( !$key ){
1048
			return;
1049
		}
1050
1051
		if( !$this->MatchChar('=') ){
1052
			return;
1053
		}
1054
1055
		$value = $this->parseEntity();
1056
		if( $value ){
1057
			return $this->NewObj2('Less_Tree_Assignment',array($key[0], $value));
1058
		}
1059
	}
1060
1061
	//
1062
	// Parse url() tokens
1063
	//
1064
	// We use a specific rule for urls, because they don't really behave like
1065
	// standard function calls. The difference is that the argument doesn't have
1066
	// to be enclosed within a string, so it can't be parsed as an Expression.
1067
	//
1068
	private function parseEntitiesUrl(){
1069
1070
1071
		if( $this->input[$this->pos] !== 'u' || !$this->matchReg('/\\Gurl\(/') ){
1072
			return;
1073
		}
1074
1075
		$value = $this->match( array('parseEntitiesQuoted','parseEntitiesVariable','/\\Gdata\:.*?[^\)]+/','/\\G(?:(?:\\\\[\(\)\'"])|[^\(\)\'"])+/') );
1076
		if( !$value ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $value of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1077
			$value = '';
1078
		}
1079
1080
1081
		$this->expectChar(')');
1082
1083
1084
		if( isset($value->value) || $value instanceof Less_Tree_Variable ){
1085
			return $this->NewObj2('Less_Tree_Url',array($value, $this->env->currentFileInfo));
1086
		}
1087
1088
		return $this->NewObj2('Less_Tree_Url', array( $this->NewObj1('Less_Tree_Anonymous',$value), $this->env->currentFileInfo) );
1089
	}
1090
1091
1092
	//
1093
	// A Variable entity, such as `@fink`, in
1094
	//
1095
	//	 width: @fink + 2px
1096
	//
1097
	// We use a different parser for variable definitions,
1098
	// see `parsers.variable`.
1099
	//
1100
	private function parseEntitiesVariable(){
1101
		$index = $this->pos;
1102
		if ($this->PeekChar('@') && ($name = $this->MatchReg('/\\G@@?[\w-]+/'))) {
1103
			return $this->NewObj3('Less_Tree_Variable', array( $name[0], $index, $this->env->currentFileInfo));
1104
		}
1105
	}
1106
1107
1108
	// A variable entity useing the protective {} e.g. @{var}
1109
	private function parseEntitiesVariableCurly() {
1110
		$index = $this->pos;
1111
1112
		if( $this->input_len > ($this->pos+1) && $this->input[$this->pos] === '@' && ($curly = $this->MatchReg('/\\G@\{([\w-]+)\}/')) ){
1113
			return $this->NewObj3('Less_Tree_Variable',array('@'.$curly[1], $index, $this->env->currentFileInfo));
1114
		}
1115
	}
1116
1117
	//
1118
	// A Hexadecimal color
1119
	//
1120
	//	 #4F3C2F
1121
	//
1122
	// `rgb` and `hsl` colors are parsed through the `entities.call` parser.
1123
	//
1124
	private function parseEntitiesColor(){
1125
		if ($this->PeekChar('#') && ($rgb = $this->MatchReg('/\\G#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/'))) {
1126
			return $this->NewObj1('Less_Tree_Color',$rgb[1]);
1127
		}
1128
	}
1129
1130
	//
1131
	// A Dimension, that is, a number and a unit
1132
	//
1133
	//	 0.5em 95%
1134
	//
1135
	private function parseEntitiesDimension(){
1136
1137
		$c = @ord($this->input[$this->pos]);
1138
1139
		//Is the first char of the dimension 0-9, '.', '+' or '-'
1140
		if (($c > 57 || $c < 43) || $c === 47 || $c == 44){
1141
			return;
1142
		}
1143
1144
		$value = $this->MatchReg('/\\G([+-]?\d*\.?\d+)(%|[a-z]+)?/');
1145
		if( $value ){
1146
1147
			if( isset($value[2]) ){
1148
				return $this->NewObj2('Less_Tree_Dimension', array($value[1],$value[2]));
1149
			}
1150
			return $this->NewObj1('Less_Tree_Dimension',$value[1]);
1151
		}
1152
	}
1153
1154
1155
	//
1156
	// A unicode descriptor, as is used in unicode-range
1157
	//
1158
	// U+0?? or U+00A1-00A9
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% 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...
1159
	//
1160
	function parseUnicodeDescriptor() {
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...
1161
		$ud = $this->MatchReg('/\\G(U\+[0-9a-fA-F?]+)(\-[0-9a-fA-F?]+)?/');
1162
		if( $ud ){
1163
			return $this->NewObj1('Less_Tree_UnicodeDescriptor', $ud[0]);
1164
		}
1165
	}
1166
1167
1168
	//
1169
	// JavaScript code to be evaluated
1170
	//
1171
	//	 `window.location.href`
1172
	//
1173
	private function parseEntitiesJavascript(){
1174
		$e = false;
1175
		$j = $this->pos;
1176
		if( $this->input[$j] === '~' ){
1177
			$j++;
1178
			$e = true;
1179
		}
1180
		if( $this->input[$j] !== '`' ){
1181
			return;
1182
		}
1183
		if( $e ){
1184
			$this->MatchChar('~');
1185
		}
1186
		$str = $this->MatchReg('/\\G`([^`]*)`/');
1187
		if( $str ){
1188
			return $this->NewObj3('Less_Tree_Javascript', array($str[1], $this->pos, $e));
1189
		}
1190
	}
1191
1192
1193
	//
1194
	// The variable part of a variable definition. Used in the `rule` parser
1195
	//
1196
	//	 @fink:
1197
	//
1198
	private function parseVariable(){
1199
		if ($this->PeekChar('@') && ($name = $this->MatchReg('/\\G(@[\w-]+)\s*:/'))) {
1200
			return $name[1];
1201
		}
1202
	}
1203
1204
1205
	//
1206
	// The variable part of a variable definition. Used in the `rule` parser
1207
	//
1208
	// @fink();
1209
	//
1210
	private function parseRulesetCall(){
1211
1212
		if( $this->input[$this->pos] === '@' && ($name = $this->MatchReg('/\\G(@[\w-]+)\s*\(\s*\)\s*;/')) ){
1213
			return $this->NewObj1('Less_Tree_RulesetCall', $name[1] );
1214
		}
1215
	}
1216
1217
1218
	//
1219
	// extend syntax - used to extend selectors
1220
	//
1221
	function parseExtend($isRule = false){
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...
1222
1223
		$index = $this->pos;
1224
		$extendList = array();
1225
1226
1227
		if( !$this->MatchReg( $isRule ? '/\\G&:extend\(/' : '/\\G:extend\(/' ) ){ return; }
1228
1229
		do{
1230
			$option = null;
1231
			$elements = array();
1232
			while( true ){
1233
				$option = $this->MatchReg('/\\G(all)(?=\s*(\)|,))/');
1234
				if( $option ){ break; }
0 ignored issues
show
Bug Best Practice introduced by
The expression $option of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1235
				$e = $this->parseElement();
1236
				if( !$e ){ break; }
1237
				$elements[] = $e;
1238
			}
1239
1240
			if( $option ){
1241
				$option = $option[1];
1242
			}
1243
1244
			$extendList[] = $this->NewObj3('Less_Tree_Extend', array( $this->NewObj1('Less_Tree_Selector',$elements), $option, $index ));
1245
1246
		}while( $this->MatchChar(",") );
1247
1248
		$this->expect('/\\G\)/');
1249
1250
		if( $isRule ){
1251
			$this->expect('/\\G;/');
1252
		}
1253
1254
		return $extendList;
1255
	}
1256
1257
1258
	//
1259
	// A Mixin call, with an optional argument list
1260
	//
1261
	//	 #mixins > .square(#fff);
1262
	//	 .rounded(4px, black);
0 ignored issues
show
Unused Code Comprehensibility introduced by
46% 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...
1263
	//	 .button;
1264
	//
1265
	// The `while` loop is there because mixins can be
1266
	// namespaced, but we only support the child and descendant
1267
	// selector for now.
1268
	//
1269
	private function parseMixinCall(){
1270
1271
		$char = $this->input[$this->pos];
1272
		if( $char !== '.' && $char !== '#' ){
1273
			return;
1274
		}
1275
1276
		$index = $this->pos;
1277
		$this->save(); // stop us absorbing part of an invalid selector
1278
1279
		$elements = $this->parseMixinCallElements();
1280
1281
		if( $elements ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $elements of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1282
1283
			if( $this->MatchChar('(') ){
1284
				$returned = $this->parseMixinArgs(true);
1285
				$args = $returned['args'];
1286
				$this->expectChar(')');
1287
			}else{
1288
				$args = array();
1289
			}
1290
1291
			$important = $this->parseImportant();
1292
1293
			if( $this->parseEnd() ){
1294
				$this->forget();
1295
				return $this->NewObj5('Less_Tree_Mixin_Call', array( $elements, $args, $index, $this->env->currentFileInfo, $important));
1296
			}
1297
		}
1298
1299
		$this->restore();
1300
	}
1301
1302
1303
	private function parseMixinCallElements(){
1304
		$elements = array();
1305
		$c = null;
1306
1307
		while( true ){
1308
			$elemIndex = $this->pos;
1309
			$e = $this->MatchReg('/\\G[#.](?:[\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/');
1310
			if( !$e ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $e of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1311
				break;
1312
			}
1313
			$elements[] = $this->NewObj4('Less_Tree_Element', array($c, $e[0], $elemIndex, $this->env->currentFileInfo));
1314
			$c = $this->MatchChar('>');
1315
		}
1316
1317
		return $elements;
1318
	}
1319
1320
1321
1322
	/**
1323
	 * @param boolean $isCall
1324
	 */
1325
	private function parseMixinArgs( $isCall ){
1326
		$expressions = array();
1327
		$argsSemiColon = array();
1328
		$isSemiColonSeperated = null;
1329
		$argsComma = array();
1330
		$expressionContainsNamed = null;
1331
		$name = null;
1332
		$returner = array('args'=>array(), 'variadic'=> false);
1333
1334
		$this->save();
1335
1336
		while( true ){
1337
			if( $isCall ){
1338
				$arg = $this->MatchFuncs( array( 'parseDetachedRuleset','parseExpression' ) );
1339
			} else {
1340
				$this->parseComments();
1341
				if( $this->input[ $this->pos ] === '.' && $this->MatchReg('/\\G\.{3}/') ){
1342
					$returner['variadic'] = true;
1343
					if( $this->MatchChar(";") && !$isSemiColonSeperated ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $isSemiColonSeperated of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
1344
						$isSemiColonSeperated = true;
1345
					}
1346
1347
					if( $isSemiColonSeperated ){
1348
						$argsSemiColon[] = array('variadic'=>true);
1349
					}else{
1350
						$argsComma[] = array('variadic'=>true);
1351
					}
1352
					break;
1353
				}
1354
				$arg = $this->MatchFuncs( array('parseEntitiesVariable','parseEntitiesLiteral','parseEntitiesKeyword') );
1355
			}
1356
1357
			if( !$arg ){
1358
				break;
1359
			}
1360
1361
1362
			$nameLoop = null;
1363
			if( $arg instanceof Less_Tree_Expression ){
1364
				$arg->throwAwayComments();
1365
			}
1366
			$value = $arg;
1367
			$val = null;
1368
1369
			if( $isCall ){
1370
				// Variable
1371
				if( property_exists($arg,'value') && count($arg->value) == 1 ){
1372
					$val = $arg->value[0];
1373
				}
1374
			} else {
1375
				$val = $arg;
1376
			}
1377
1378
1379
			if( $val instanceof Less_Tree_Variable ){
1380
1381
				if( $this->MatchChar(':') ){
1382
					if( $expressions ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $expressions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1383
						if( $isSemiColonSeperated ){
1384
							$this->Error('Cannot mix ; and , as delimiter types');
1385
						}
1386
						$expressionContainsNamed = true;
1387
					}
1388
1389
					// we do not support setting a ruleset as a default variable - it doesn't make sense
1390
					// However if we do want to add it, there is nothing blocking it, just don't error
1391
					// and remove isCall dependency below
1392
					$value = null;
1393
					if( $isCall ){
1394
						$value = $this->parseDetachedRuleset();
1395
					}
1396
					if( !$value ){
1397
						$value = $this->parseExpression();
1398
					}
1399
1400
					if( !$value ){
1401
						if( $isCall ){
1402
							$this->Error('could not understand value for named argument');
1403
						} else {
1404
							$this->restore();
1405
							$returner['args'] = array();
1406
							return $returner;
1407
						}
1408
					}
1409
1410
					$nameLoop = ($name = $val->name);
1411
				}elseif( !$isCall && $this->MatchReg('/\\G\.{3}/') ){
1412
					$returner['variadic'] = true;
1413
					if( $this->MatchChar(";") && !$isSemiColonSeperated ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $isSemiColonSeperated of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
1414
						$isSemiColonSeperated = true;
1415
					}
1416
					if( $isSemiColonSeperated ){
1417
						$argsSemiColon[] = array('name'=> $arg->name, 'variadic' => true);
1418
					}else{
1419
						$argsComma[] = array('name'=> $arg->name, 'variadic' => true);
1420
					}
1421
					break;
1422
				}elseif( !$isCall ){
1423
					$name = $nameLoop = $val->name;
1424
					$value = null;
1425
				}
1426
			}
1427
1428
			if( $value ){
1429
				$expressions[] = $value;
1430
			}
1431
1432
			$argsComma[] = array('name'=>$nameLoop, 'value'=>$value );
1433
1434
			if( $this->MatchChar(',') ){
1435
				continue;
1436
			}
1437
1438
			if( $this->MatchChar(';') || $isSemiColonSeperated ){
1439
1440
				if( $expressionContainsNamed ){
1441
					$this->Error('Cannot mix ; and , as delimiter types');
1442
				}
1443
1444
				$isSemiColonSeperated = true;
1445
1446
				if( count($expressions) > 1 ){
1447
					$value = $this->NewObj1('Less_Tree_Value', $expressions);
1448
				}
1449
				$argsSemiColon[] = array('name'=>$name, 'value'=>$value );
1450
1451
				$name = null;
1452
				$expressions = array();
1453
				$expressionContainsNamed = false;
1454
			}
1455
		}
1456
1457
		$this->forget();
1458
		$returner['args'] = ($isSemiColonSeperated ? $argsSemiColon : $argsComma);
1459
		return $returner;
1460
	}
1461
1462
1463
1464
	//
1465
	// A Mixin definition, with a list of parameters
1466
	//
1467
	//	 .rounded (@radius: 2px, @color) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% 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...
1468
	//		...
1469
	//	 }
1470
	//
1471
	// Until we have a finer grained state-machine, we have to
1472
	// do a look-ahead, to make sure we don't have a mixin call.
1473
	// See the `rule` function for more information.
1474
	//
1475
	// We start by matching `.rounded (`, and then proceed on to
1476
	// the argument list, which has optional default values.
1477
	// We store the parameters in `params`, with a `value` key,
1478
	// if there is a value, such as in the case of `@radius`.
1479
	//
1480
	// Once we've got our params list, and a closing `)`, we parse
1481
	// the `{...}` block.
1482
	//
1483
	private function parseMixinDefinition(){
1484
		$cond = null;
1485
1486
		$char = $this->input[$this->pos];
1487
		if( ($char !== '.' && $char !== '#') || ($char === '{' && $this->PeekReg('/\\G[^{]*\}/')) ){
1488
			return;
1489
		}
1490
1491
		$this->save();
1492
1493
		$match = $this->MatchReg('/\\G([#.](?:[\w-]|\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/');
1494
		if( $match ){
1495
			$name = $match[1];
1496
1497
			$argInfo = $this->parseMixinArgs( false );
1498
			$params = $argInfo['args'];
1499
			$variadic = $argInfo['variadic'];
1500
1501
1502
			// .mixincall("@{a}");
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% 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...
1503
			// looks a bit like a mixin definition..
1504
			// also
1505
			// .mixincall(@a: {rule: set;});
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% 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...
1506
			// so we have to be nice and restore
1507
			if( !$this->MatchChar(')') ){
1508
				$this->furthest = $this->pos;
1509
				$this->restore();
1510
				return;
1511
			}
1512
1513
1514
			$this->parseComments();
1515
1516
			if ($this->MatchReg('/\\Gwhen/')) { // Guard
1517
				$cond = $this->expect('parseConditions', 'Expected conditions');
1518
			}
1519
1520
			$ruleset = $this->parseBlock();
1521
1522
			if( is_array($ruleset) ){
1523
				$this->forget();
1524
				return $this->NewObj5('Less_Tree_Mixin_Definition', array( $name, $params, $ruleset, $cond, $variadic));
1525
			}
1526
1527
			$this->restore();
1528
		}else{
1529
			$this->forget();
1530
		}
1531
	}
1532
1533
	//
1534
	// Entities are the smallest recognized token,
1535
	// and can be found inside a rule's value.
1536
	//
1537
	private function parseEntity(){
1538
1539
		return $this->MatchFuncs( array('parseEntitiesLiteral','parseEntitiesVariable','parseEntitiesUrl','parseEntitiesCall','parseEntitiesKeyword','parseEntitiesJavascript','parseComment') );
1540
	}
1541
1542
	//
1543
	// A Rule terminator. Note that we use `peek()` to check for '}',
1544
	// because the `block` rule will be expecting it, but we still need to make sure
1545
	// it's there, if ';' was ommitted.
1546
	//
1547
	private function parseEnd(){
1548
		return $this->MatchChar(';') || $this->PeekChar('}');
1549
	}
1550
1551
	//
1552
	// IE's alpha function
1553
	//
1554
	//	 alpha(opacity=88)
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...
1555
	//
1556
	private function parseAlpha(){
1557
1558
		if ( ! $this->MatchReg('/\\G\(opacity=/i')) {
1559
			return;
1560
		}
1561
1562
		$value = $this->MatchReg('/\\G[0-9]+/');
1563
		if( $value ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $value of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1564
			$value = $value[0];
1565
		}else{
1566
			$value = $this->parseEntitiesVariable();
1567
			if( !$value ){
1568
				return;
1569
			}
1570
		}
1571
1572
		$this->expectChar(')');
1573
		return $this->NewObj1('Less_Tree_Alpha',$value);
1574
	}
1575
1576
1577
	//
1578
	// A Selector Element
1579
	//
1580
	//	 div
1581
	//	 + h1
1582
	//	 #socks
1583
	//	 input[type="text"]
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...
1584
	//
1585
	// Elements are the building blocks for Selectors,
1586
	// they are made out of a `Combinator` (see combinator rule),
1587
	// and an element name, such as a tag a class, or `*`.
1588
	//
1589
	private function parseElement(){
1590
		$c = $this->parseCombinator();
1591
		$index = $this->pos;
1592
1593
		$e = $this->match( array('/\\G(?:\d+\.\d+|\d+)%/', '/\\G(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/',
1594
			'#*', '#&', 'parseAttribute', '/\\G\([^()@]+\)/', '/\\G[\.#](?=@)/', 'parseEntitiesVariableCurly') );
1595
1596
		if( is_null($e) ){
1597
			$this->save();
1598
			if( $this->MatchChar('(') ){
1599
				if( ($v = $this->parseSelector()) && $this->MatchChar(')') ){
1600
					$e = $this->NewObj1('Less_Tree_Paren',$v);
1601
					$this->forget();
1602
				}else{
1603
					$this->restore();
1604
				}
1605
			}else{
1606
				$this->forget();
1607
			}
1608
		}
1609
1610
		if( !is_null($e) ){
1611
			return $this->NewObj4('Less_Tree_Element',array( $c, $e, $index, $this->env->currentFileInfo));
1612
		}
1613
	}
1614
1615
	//
1616
	// Combinators combine elements together, in a Selector.
1617
	//
1618
	// Because our parser isn't white-space sensitive, special care
1619
	// has to be taken, when parsing the descendant combinator, ` `,
1620
	// as it's an empty space. We have to check the previous character
1621
	// in the input, to see if it's a ` ` character.
1622
	//
1623
	private function parseCombinator(){
1624
		if( $this->pos < $this->input_len ){
1625
			$c = $this->input[$this->pos];
1626
			if ($c === '>' || $c === '+' || $c === '~' || $c === '|' || $c === '^' ){
1627
1628
				$this->pos++;
1629
				if( $this->input[$this->pos] === '^' ){
1630
					$c = '^^';
1631
					$this->pos++;
1632
				}
1633
1634
				$this->skipWhitespace(0);
1635
1636
				return $c;
1637
			}
1638
1639
			if( $this->pos > 0 && $this->isWhitespace(-1) ){
1640
				return ' ';
1641
			}
1642
		}
1643
	}
1644
1645
	//
1646
	// A CSS selector (see selector below)
1647
	// with less extensions e.g. the ability to extend and guard
1648
	//
1649
	private function parseLessSelector(){
1650
		return $this->parseSelector(true);
1651
	}
1652
1653
	//
1654
	// A CSS Selector
1655
	//
1656
	//	 .class > div + h1
1657
	//	 li a:hover
1658
	//
1659
	// Selectors are made out of one or more Elements, see above.
1660
	//
1661
	private function parseSelector( $isLess = false ){
1662
		$elements = array();
1663
		$extendList = array();
1664
		$condition = null;
1665
		$when = false;
1666
		$extend = false;
1667
		$e = null;
1668
		$c = null;
1669
		$index = $this->pos;
1670
1671
		while( ($isLess && ($extend = $this->parseExtend())) || ($isLess && ($when = $this->MatchReg('/\\Gwhen/') )) || ($e = $this->parseElement()) ){
1672
			if( $when ){
1673
				$condition = $this->expect('parseConditions', 'expected condition');
1674
			}elseif( $condition ){
0 ignored issues
show
Unused Code introduced by
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
1675
				//error("CSS guard can only be used at the end of selector");
1676
			}elseif( $extend ){
1677
				$extendList = array_merge($extendList,$extend);
1678
			}else{
1679
				//if( count($extendList) ){
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% 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...
1680
					//error("Extend can only be used at the end of selector");
1681
				//}
1682
				if( $this->pos < $this->input_len ){
1683
					$c = $this->input[ $this->pos ];
1684
				}
1685
				$elements[] = $e;
1686
				$e = null;
1687
			}
1688
1689
			if( $c === '{' || $c === '}' || $c === ';' || $c === ',' || $c === ')') { break; }
1690
		}
1691
1692
		if( $elements ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $elements of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1693
			return $this->NewObj5('Less_Tree_Selector',array($elements, $extendList, $condition, $index, $this->env->currentFileInfo));
1694
		}
1695
		if( $extendList ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $extendList of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1696
			$this->Error('Extend must be used to extend a selector, it cannot be used on its own');
1697
		}
1698
	}
1699
1700
	private function parseTag(){
1701
		return ( $tag = $this->MatchReg('/\\G[A-Za-z][A-Za-z-]*[0-9]?/') ) ? $tag : $this->MatchChar('*');
1702
	}
1703
1704
	private function parseAttribute(){
1705
1706
		$val = null;
1707
1708
		if( !$this->MatchChar('[') ){
1709
			return;
1710
		}
1711
1712
		$key = $this->parseEntitiesVariableCurly();
1713
		if( !$key ){
1714
			$key = $this->expect('/\\G(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\\\.)+/');
1715
		}
1716
1717
		$op = $this->MatchReg('/\\G[|~*$^]?=/');
1718
		if( $op ){
1719
			$val = $this->match( array('parseEntitiesQuoted','/\\G[0-9]+%/','/\\G[\w-]+/','parseEntitiesVariableCurly') );
1720
		}
1721
1722
		$this->expectChar(']');
1723
1724
		return $this->NewObj3('Less_Tree_Attribute',array( $key, $op[0], $val));
1725
	}
1726
1727
	//
1728
	// The `block` rule is used by `ruleset` and `mixin.definition`.
1729
	// It's a wrapper around the `primary` rule, with added `{}`.
1730
	//
1731
	private function parseBlock(){
1732
		if( $this->MatchChar('{') ){
1733
			$content = $this->parsePrimary();
1734
			if( $this->MatchChar('}') ){
1735
				return $content;
1736
			}
1737
		}
1738
	}
1739
1740
	private function parseBlockRuleset(){
1741
		$block = $this->parseBlock();
1742
1743
		if( $block ){
1744
			$block = $this->NewObj2('Less_Tree_Ruleset',array( null, $block));
1745
		}
1746
1747
		return $block;
1748
	}
1749
1750
	private function parseDetachedRuleset(){
1751
		$blockRuleset = $this->parseBlockRuleset();
1752
		if( $blockRuleset ){
1753
			return $this->NewObj1('Less_Tree_DetachedRuleset',$blockRuleset);
1754
		}
1755
	}
1756
1757
	//
1758
	// div, .class, body > p {...}
0 ignored issues
show
Unused Code Comprehensibility introduced by
36% 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...
1759
	//
1760
	private function parseRuleset(){
1761
		$selectors = array();
1762
1763
		$this->save();
1764
1765
		while( true ){
1766
			$s = $this->parseLessSelector();
1767
			if( !$s ){
1768
				break;
1769
			}
1770
			$selectors[] = $s;
1771
			$this->parseComments();
1772
1773
			if( $s->condition && count($selectors) > 1 ){
1774
				$this->Error('Guards are only currently allowed on a single selector.');
1775
			}
1776
1777
			if( !$this->MatchChar(',') ){
1778
				break;
1779
			}
1780
			if( $s->condition ){
1781
				$this->Error('Guards are only currently allowed on a single selector.');
1782
			}
1783
			$this->parseComments();
1784
		}
1785
1786
1787
		if( $selectors ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $selectors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1788
			$rules = $this->parseBlock();
1789
			if( is_array($rules) ){
1790
				$this->forget();
1791
				return $this->NewObj2('Less_Tree_Ruleset',array( $selectors, $rules)); //Less_Environment::$strictImports
1792
			}
1793
		}
1794
1795
		// Backtrack
1796
		$this->furthest = $this->pos;
1797
		$this->restore();
1798
	}
1799
1800
	/**
1801
	 * Custom less.php parse function for finding simple name-value css pairs
1802
	 * ex: width:100px;
1803
	 *
1804
	 */
1805
	private function parseNameValue(){
1806
1807
		$index = $this->pos;
1808
		$this->save();
1809
1810
1811
		//$match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*((?:\'")?[a-zA-Z0-9\-% \.,!]+?(?:\'")?)\s*([;}])/');
1812
		$match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*([\'"]?[#a-zA-Z0-9\-%\.,]+?[\'"]?) *(! *important)?\s*([;}])/');
1813
		if( $match ){
1814
1815
			if( $match[4] == '}' ){
1816
				$this->pos = $index + strlen($match[0])-1;
1817
			}
1818
1819
			if( $match[3] ){
1820
				$match[2] .= ' !important';
1821
			}
1822
1823
			return $this->NewObj4('Less_Tree_NameValue',array( $match[1], $match[2], $index, $this->env->currentFileInfo));
1824
		}
1825
1826
		$this->restore();
1827
	}
1828
1829
1830
	private function parseRule( $tryAnonymous = null ){
1831
1832
		$merge = false;
1833
		$startOfRule = $this->pos;
1834
1835
		$c = $this->input[$this->pos];
1836
		if( $c === '.' || $c === '#' || $c === '&' ){
1837
			return;
1838
		}
1839
1840
		$this->save();
1841
		$name = $this->MatchFuncs( array('parseVariable','parseRuleProperty'));
1842
1843
		if( $name ){
1844
1845
			$isVariable = is_string($name);
1846
1847
			$value = null;
1848
			if( $isVariable ){
1849
				$value = $this->parseDetachedRuleset();
1850
			}
1851
1852
			$important = null;
1853
			if( !$value ){
1854
1855
				// prefer to try to parse first if its a variable or we are compressing
1856
				// but always fallback on the other one
1857
				//if( !$tryAnonymous && is_string($name) && $name[0] === '@' ){
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% 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...
1858
				if( !$tryAnonymous && (Less_Parser::$options['compress'] || $isVariable) ){
1859
					$value = $this->MatchFuncs( array('parseValue','parseAnonymousValue'));
1860
				}else{
1861
					$value = $this->MatchFuncs( array('parseAnonymousValue','parseValue'));
1862
				}
1863
1864
				$important = $this->parseImportant();
1865
1866
				// a name returned by this.ruleProperty() is always an array of the form:
1867
				// [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"]
0 ignored issues
show
Unused Code Comprehensibility introduced by
48% 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...
1868
				// where each item is a tree.Keyword or tree.Variable
1869
				if( !$isVariable && is_array($name) ){
1870
					$nm = array_pop($name);
1871
					if( $nm->value ){
1872
						$merge = $nm->value;
1873
					}
1874
				}
1875
			}
1876
1877
1878
			if( $value && $this->parseEnd() ){
1879
				$this->forget();
1880
				return $this->NewObj6('Less_Tree_Rule',array( $name, $value, $important, $merge, $startOfRule, $this->env->currentFileInfo));
1881
			}else{
1882
				$this->furthest = $this->pos;
1883
				$this->restore();
1884
				if( $value && !$tryAnonymous ){
1885
					return $this->parseRule(true);
1886
				}
1887
			}
1888
		}else{
1889
			$this->forget();
1890
		}
1891
	}
1892
1893
	function parseAnonymousValue(){
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...
1894
1895
		if( preg_match('/\\G([^@+\/\'"*`(;{}-]*);/',$this->input, $match, 0, $this->pos) ){
1896
			$this->pos += strlen($match[1]);
1897
			return $this->NewObj1('Less_Tree_Anonymous',$match[1]);
1898
		}
1899
	}
1900
1901
	//
1902
	// An @import directive
1903
	//
1904
	//	 @import "lib";
1905
	//
1906
	// Depending on our environment, importing is done differently:
1907
	// In the browser, it's an XHR request, in Node, it would be a
1908
	// file-system operation. The function used for importing is
1909
	// stored in `import`, which we pass to the Import constructor.
1910
	//
1911
	private function parseImport(){
1912
1913
		$this->save();
1914
1915
		$dir = $this->MatchReg('/\\G@import?\s+/');
1916
1917
		if( $dir ){
1918
			$options = $this->parseImportOptions();
1919
			$path = $this->MatchFuncs( array('parseEntitiesQuoted','parseEntitiesUrl'));
1920
1921
			if( $path ){
1922
				$features = $this->parseMediaFeatures();
1923
				if( $this->MatchChar(';') ){
1924
					if( $features ){
1925
						$features = $this->NewObj1('Less_Tree_Value',$features);
1926
					}
1927
1928
					$this->forget();
1929
					return $this->NewObj5('Less_Tree_Import',array( $path, $features, $options, $this->pos, $this->env->currentFileInfo));
1930
				}
1931
			}
1932
		}
1933
1934
		$this->restore();
1935
	}
1936
1937
	private function parseImportOptions(){
1938
1939
		$options = array();
1940
1941
		// list of options, surrounded by parens
1942
		if( !$this->MatchChar('(') ){
1943
			return $options;
1944
		}
1945
		do{
1946
			$optionName = $this->parseImportOption();
1947
			if( $optionName ){
1948
				$value = true;
1949
				switch( $optionName ){
1950
					case "css":
1951
						$optionName = "less";
1952
						$value = false;
1953
					break;
1954
					case "once":
1955
						$optionName = "multiple";
1956
						$value = false;
1957
					break;
1958
				}
1959
				$options[$optionName] = $value;
1960
				if( !$this->MatchChar(',') ){ break; }
1961
			}
1962
		}while( $optionName );
1963
		$this->expectChar(')');
1964
		return $options;
1965
	}
1966
1967
	private function parseImportOption(){
1968
		$opt = $this->MatchReg('/\\G(less|css|multiple|once|inline|reference)/');
1969
		if( $opt ){
1970
			return $opt[1];
1971
		}
1972
	}
1973
1974
	private function parseMediaFeature() {
1975
		$nodes = array();
1976
1977
		do{
1978
			$e = $this->MatchFuncs(array('parseEntitiesKeyword','parseEntitiesVariable'));
1979
			if( $e ){
1980
				$nodes[] = $e;
1981
			} elseif ($this->MatchChar('(')) {
1982
				$p = $this->parseProperty();
1983
				$e = $this->parseValue();
1984
				if ($this->MatchChar(')')) {
1985
					if ($p && $e) {
1986
						$r = $this->NewObj7('Less_Tree_Rule', array( $p, $e, null, null, $this->pos, $this->env->currentFileInfo, true));
1987
						$nodes[] = $this->NewObj1('Less_Tree_Paren',$r);
1988
					} elseif ($e) {
1989
						$nodes[] = $this->NewObj1('Less_Tree_Paren',$e);
1990
					} else {
1991
						return null;
1992
					}
1993
				} else
1994
					return null;
1995
			}
1996
		} while ($e);
1997
1998
		if ($nodes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $nodes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1999
			return $this->NewObj1('Less_Tree_Expression',$nodes);
2000
		}
2001
	}
2002
2003
	private function parseMediaFeatures() {
2004
		$features = array();
2005
2006
		do{
2007
			$e = $this->parseMediaFeature();
2008
			if( $e ){
2009
				$features[] = $e;
2010
				if (!$this->MatchChar(',')) break;
2011
			}else{
2012
				$e = $this->parseEntitiesVariable();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $e is correct as $this->parseEntitiesVariable() (which targets Less_Parser::parseEntitiesVariable()) seems to always return null.

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

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

}

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

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

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

Loading history...
2013
				if( $e ){
2014
					$features[] = $e;
2015
					if (!$this->MatchChar(',')) break;
2016
				}
2017
			}
2018
		} while ($e);
2019
2020
		return $features ? $features : null;
2021
	}
2022
2023
	private function parseMedia() {
2024
		if( $this->MatchReg('/\\G@media/') ){
2025
			$features = $this->parseMediaFeatures();
2026
			$rules = $this->parseBlock();
2027
2028
			if( is_array($rules) ){
2029
				return $this->NewObj4('Less_Tree_Media',array( $rules, $features, $this->pos, $this->env->currentFileInfo));
2030
			}
2031
		}
2032
	}
2033
2034
2035
	//
2036
	// A CSS Directive
2037
	//
2038
	// @charset "utf-8";
2039
	//
2040
	private function parseDirective(){
2041
2042
		if( !$this->PeekChar('@') ){
2043
			return;
2044
		}
2045
2046
		$rules = null;
2047
		$index = $this->pos;
2048
		$hasBlock = true;
2049
		$hasIdentifier = false;
2050
		$hasExpression = false;
2051
		$hasUnknown = false;
2052
2053
2054
		$value = $this->MatchFuncs(array('parseImport','parseMedia'));
2055
		if( $value ){
2056
			return $value;
2057
		}
2058
2059
		$this->save();
2060
2061
		$name = $this->MatchReg('/\\G@[a-z-]+/');
2062
2063
		if( !$name ) return;
2064
		$name = $name[0];
2065
2066
2067
		$nonVendorSpecificName = $name;
2068
		$pos = strpos($name,'-', 2);
2069
		if( $name[1] == '-' && $pos > 0 ){
2070
			$nonVendorSpecificName = "@" . substr($name, $pos + 1);
2071
		}
2072
2073
2074
		switch( $nonVendorSpecificName ){
2075
			/*
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% 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...
2076
			case "@font-face":
2077
			case "@viewport":
2078
			case "@top-left":
2079
			case "@top-left-corner":
2080
			case "@top-center":
2081
			case "@top-right":
2082
			case "@top-right-corner":
2083
			case "@bottom-left":
2084
			case "@bottom-left-corner":
2085
			case "@bottom-center":
2086
			case "@bottom-right":
2087
			case "@bottom-right-corner":
2088
			case "@left-top":
2089
			case "@left-middle":
2090
			case "@left-bottom":
2091
			case "@right-top":
2092
			case "@right-middle":
2093
			case "@right-bottom":
2094
			hasBlock = true;
2095
			break;
2096
			*/
2097
			case "@charset":
2098
				$hasIdentifier = true;
2099
				$hasBlock = false;
2100
				break;
2101
			case "@namespace":
2102
				$hasExpression = true;
2103
				$hasBlock = false;
2104
				break;
2105
			case "@keyframes":
2106
				$hasIdentifier = true;
2107
				break;
2108
			case "@host":
2109
			case "@page":
2110
			case "@document":
2111
			case "@supports":
2112
				$hasUnknown = true;
2113
				break;
2114
		}
2115
2116
		if( $hasIdentifier ){
2117
			$value = $this->parseEntity();
2118
			if( !$value ){
2119
				$this->error("expected " . $name . " identifier");
2120
			}
2121
		} else if( $hasExpression ){
2122
			$value = $this->parseExpression();
2123
			if( !$value ){
2124
				$this->error("expected " . $name. " expression");
2125
			}
2126
		} else if ($hasUnknown) {
2127
2128
			$value = $this->MatchReg('/\\G[^{;]+/');
2129
			if( $value ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $value of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2130
				$value = $this->NewObj1('Less_Tree_Anonymous',trim($value[0]));
2131
			}
2132
		}
2133
2134
		if( $hasBlock ){
2135
			$rules = $this->parseBlockRuleset();
2136
		}
2137
2138
		if( $rules || (!$hasBlock && $value && $this->MatchChar(';'))) {
2139
			$this->forget();
2140
			return $this->NewObj5('Less_Tree_Directive',array($name, $value, $rules, $index, $this->env->currentFileInfo));
2141
		}
2142
2143
		$this->restore();
2144
	}
2145
2146
2147
	//
2148
	// A Value is a comma-delimited list of Expressions
2149
	//
2150
	//	 font-family: Baskerville, Georgia, serif;
2151
	//
2152
	// In a Rule, a Value represents everything after the `:`,
2153
	// and before the `;`.
2154
	//
2155
	private function parseValue(){
2156
		$expressions = array();
2157
2158
		do{
2159
			$e = $this->parseExpression();
2160
			if( $e ){
2161
				$expressions[] = $e;
2162
				if (! $this->MatchChar(',')) {
2163
					break;
2164
				}
2165
			}
2166
		}while($e);
2167
2168
		if( $expressions ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $expressions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2169
			return $this->NewObj1('Less_Tree_Value',$expressions);
2170
		}
2171
	}
2172
2173
	private function parseImportant (){
2174
		if( $this->PeekChar('!') && $this->MatchReg('/\\G! *important/') ){
2175
			return ' !important';
2176
		}
2177
	}
2178
2179
	private function parseSub (){
2180
2181
		if( $this->MatchChar('(') ){
2182
			$a = $this->parseAddition();
2183
			if( $a ){
2184
				$this->expectChar(')');
2185
				return $this->NewObj2('Less_Tree_Expression',array( array($a), true) ); //instead of $e->parens = true so the value is cached
2186
			}
2187
		}
2188
	}
2189
2190
2191
	/**
2192
	 * Parses multiplication operation
2193
	 *
2194
	 * @return Less_Tree_Operation|null
2195
	 */
2196
	function parseMultiplication(){
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...
2197
2198
		$return = $m = $this->parseOperand();
2199
		if( $return ){
2200
			while( true ){
2201
2202
				$isSpaced = $this->isWhitespace( -1 );
2203
2204
				if( $this->PeekReg('/\\G\/[*\/]/') ){
2205
					break;
2206
				}
2207
2208
				$op = $this->MatchChar('/');
2209
				if( !$op ){
2210
					$op = $this->MatchChar('*');
2211
					if( !$op ){
2212
						break;
2213
					}
2214
				}
2215
2216
				$a = $this->parseOperand();
2217
2218
				if(!$a) { break; }
2219
2220
				$m->parensInOp = true;
2221
				$a->parensInOp = true;
2222
				$return = $this->NewObj3('Less_Tree_Operation',array( $op, array( $return, $a ), $isSpaced) );
2223
			}
2224
		}
2225
		return $return;
2226
2227
	}
2228
2229
2230
	/**
2231
	 * Parses an addition operation
2232
	 *
2233
	 * @return Less_Tree_Operation|null
2234
	 */
2235
	private function parseAddition (){
2236
2237
		$return = $m = $this->parseMultiplication();
2238
		if( $return ){
2239
			while( true ){
2240
2241
				$isSpaced = $this->isWhitespace( -1 );
2242
2243
				$op = $this->MatchReg('/\\G[-+]\s+/');
2244
				if( $op ){
2245
					$op = $op[0];
2246
				}else{
2247
					if( !$isSpaced ){
2248
						$op = $this->match(array('#+','#-'));
2249
					}
2250
					if( !$op ){
2251
						break;
2252
					}
2253
				}
2254
2255
				$a = $this->parseMultiplication();
2256
				if( !$a ){
2257
					break;
2258
				}
2259
2260
				$m->parensInOp = true;
2261
				$a->parensInOp = true;
2262
				$return = $this->NewObj3('Less_Tree_Operation',array($op, array($return, $a), $isSpaced));
2263
			}
2264
		}
2265
2266
		return $return;
2267
	}
2268
2269
2270
	/**
2271
	 * Parses the conditions
2272
	 *
2273
	 * @return Less_Tree_Condition|null
2274
	 */
2275
	private function parseConditions() {
2276
		$index = $this->pos;
2277
		$return = $a = $this->parseCondition();
2278
		if( $a ){
2279
			while( true ){
2280
				if( !$this->PeekReg('/\\G,\s*(not\s*)?\(/') ||  !$this->MatchChar(',') ){
2281
					break;
2282
				}
2283
				$b = $this->parseCondition();
2284
				if( !$b ){
2285
					break;
2286
				}
2287
2288
				$return = $this->NewObj4('Less_Tree_Condition',array('or', $return, $b, $index));
2289
			}
2290
			return $return;
2291
		}
2292
	}
2293
2294
	private function parseCondition() {
2295
		$index = $this->pos;
2296
		$negate = false;
2297
		$c = null;
2298
2299
		if ($this->MatchReg('/\\Gnot/')) $negate = true;
2300
		$this->expectChar('(');
2301
		$a = $this->MatchFuncs(array('parseAddition','parseEntitiesKeyword','parseEntitiesQuoted'));
2302
2303
		if( $a ){
2304
			$op = $this->MatchReg('/\\G(?:>=|<=|=<|[<=>])/');
2305
			if( $op ){
2306
				$b = $this->MatchFuncs(array('parseAddition','parseEntitiesKeyword','parseEntitiesQuoted'));
2307 View Code Duplication
				if( $b ){
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2308
					$c = $this->NewObj5('Less_Tree_Condition',array($op[0], $a, $b, $index, $negate));
2309
				} else {
2310
					$this->Error('Unexpected expression');
2311
				}
2312 View Code Duplication
			} else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2313
				$k = $this->NewObj1('Less_Tree_Keyword','true');
2314
				$c = $this->NewObj5('Less_Tree_Condition',array('=', $a, $k, $index, $negate));
2315
			}
2316
			$this->expectChar(')');
2317
			return $this->MatchReg('/\\Gand/') ? $this->NewObj3('Less_Tree_Condition',array('and', $c, $this->parseCondition())) : $c;
2318
		}
2319
	}
2320
2321
	/**
2322
	 * An operand is anything that can be part of an operation,
2323
	 * such as a Color, or a Variable
2324
	 *
2325
	 */
2326
	private function parseOperand (){
2327
2328
		$negate = false;
2329
		$offset = $this->pos+1;
2330
		if( $offset >= $this->input_len ){
2331
			return;
2332
		}
2333
		$char = $this->input[$offset];
2334
		if( $char === '@' || $char === '(' ){
2335
			$negate = $this->MatchChar('-');
2336
		}
2337
2338
		$o = $this->MatchFuncs(array('parseSub','parseEntitiesDimension','parseEntitiesColor','parseEntitiesVariable','parseEntitiesCall'));
2339
2340
		if( $negate ){
2341
			$o->parensInOp = true;
2342
			$o = $this->NewObj1('Less_Tree_Negative',$o);
2343
		}
2344
2345
		return $o;
2346
	}
2347
2348
2349
	/**
2350
	 * Expressions either represent mathematical operations,
2351
	 * or white-space delimited Entities.
2352
	 *
2353
	 *	 1px solid black
2354
	 *	 @var * 2
2355
	 *
2356
	 * @return Less_Tree_Expression|null
2357
	 */
2358
	private function parseExpression (){
2359
		$entities = array();
2360
2361
		do{
2362
			$e = $this->MatchFuncs(array('parseAddition','parseEntity'));
2363
			if( $e ){
2364
				$entities[] = $e;
2365
				// operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here
2366
				if( !$this->PeekReg('/\\G\/[\/*]/') ){
2367
					$delim = $this->MatchChar('/');
2368
					if( $delim ){
2369
						$entities[] = $this->NewObj1('Less_Tree_Anonymous',$delim);
2370
					}
2371
				}
2372
			}
2373
		}while($e);
2374
2375
		if( $entities ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $entities of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2376
			return $this->NewObj1('Less_Tree_Expression',$entities);
2377
		}
2378
	}
2379
2380
2381
	/**
2382
	 * Parse a property
2383
	 * eg: 'min-width', 'orientation', etc
2384
	 *
2385
	 * @return string
2386
	 */
2387
	private function parseProperty (){
2388
		$name = $this->MatchReg('/\\G(\*?-?[_a-zA-Z0-9-]+)\s*:/');
2389
		if( $name ){
2390
			return $name[1];
2391
		}
2392
	}
2393
2394
2395
	/**
2396
	 * Parse a rule property
2397
	 * eg: 'color', 'width', 'height', etc
2398
	 *
2399
	 * @return string
2400
	 */
2401
	private function parseRuleProperty(){
2402
		$offset = $this->pos;
2403
		$name = array();
2404
		$index = array();
2405
		$length = 0;
2406
2407
2408
		$this->rulePropertyMatch('/\\G(\*?)/', $offset, $length, $index, $name );
2409
		while( $this->rulePropertyMatch('/\\G((?:[\w-]+)|(?:@\{[\w-]+\}))/', $offset, $length, $index, $name )); // !
2410
2411
		if( (count($name) > 1) && $this->rulePropertyMatch('/\\G\s*((?:\+_|\+)?)\s*:/', $offset, $length, $index, $name) ){
2412
			// at last, we have the complete match now. move forward,
2413
			// convert name particles to tree objects and return:
2414
			$this->skipWhitespace($length);
2415
2416
			if( $name[0] === '' ){
2417
				array_shift($name);
2418
				array_shift($index);
2419
			}
2420
			foreach($name as $k => $s ){
2421
				if( !$s || $s[0] !== '@' ){
2422
					$name[$k] = $this->NewObj1('Less_Tree_Keyword',$s);
2423
				}else{
2424
					$name[$k] = $this->NewObj3('Less_Tree_Variable',array('@' . substr($s,2,-1), $index[$k], $this->env->currentFileInfo));
2425
				}
2426
			}
2427
			return $name;
2428
		}
2429
2430
2431
	}
2432
2433
	private function rulePropertyMatch( $re, &$offset, &$length,  &$index, &$name ){
2434
		preg_match($re, $this->input, $a, 0, $offset);
2435
		if( $a ){
0 ignored issues
show
Bug Best Practice introduced by
The expression $a of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2436
			$index[] = $this->pos + $length;
2437
			$length += strlen($a[0]);
2438
			$offset += strlen($a[0]);
2439
			$name[] = $a[1];
2440
			return true;
2441
		}
2442
	}
2443
2444
	public static function serializeVars( $vars ){
2445
		$s = '';
2446
2447
		foreach($vars as $name => $value){
2448
			$s .= (($name[0] === '@') ? '' : '@') . $name .': '. $value . ((substr($value,-1) === ';') ? '' : ';');
2449
		}
2450
2451
		return $s;
2452
	}
2453
2454
2455
	/**
2456
	 * Some versions of php have trouble with method_exists($a,$b) if $a is not an object
2457
	 *
2458
	 * @param string $b
2459
	 */
2460
	public static function is_method($a,$b){
2461
		return is_object($a) && method_exists($a,$b);
2462
	}
2463
2464
2465
	/**
2466
	 * Round numbers similarly to javascript
2467
	 * eg: 1.499999 to 1 instead of 2
2468
	 *
2469
	 */
2470
	public static function round($i, $precision = 0){
2471
2472
		$precision = pow(10,$precision);
2473
		$i = $i*$precision;
2474
2475
		$ceil = ceil($i);
2476
		$floor = floor($i);
2477
		if( ($ceil - $i) <= ($i - $floor) ){
2478
			return $ceil/$precision;
2479
		}else{
2480
			return $floor/$precision;
2481
		}
2482
	}
2483
2484
2485
	/**
2486
	 * Create Less_Tree_* objects and optionally generate a cache string
2487
	 *
2488
	 * @return mixed
2489
	 */
2490
	public function NewObj0($class){
2491
		$obj = new $class();
2492
		if( $this->CacheEnabled() ){
2493
			$obj->cache_string = ' new '.$class.'()';
2494
		}
2495
		return $obj;
2496
	}
2497
2498
	public function NewObj1($class, $arg){
2499
		$obj = new $class( $arg );
2500
		if( $this->CacheEnabled() ){
2501
			$obj->cache_string = ' new '.$class.'('.Less_Parser::ArgString($arg).')';
2502
		}
2503
		return $obj;
2504
	}
2505
2506
	public function NewObj2($class, $args){
2507
		$obj = new $class( $args[0], $args[1] );
2508
		if( $this->CacheEnabled() ){
2509
			$this->ObjCache( $obj, $class, $args);
2510
		}
2511
		return $obj;
2512
	}
2513
2514
	public function NewObj3($class, $args){
2515
		$obj = new $class( $args[0], $args[1], $args[2] );
2516
		if( $this->CacheEnabled() ){
2517
			$this->ObjCache( $obj, $class, $args);
2518
		}
2519
		return $obj;
2520
	}
2521
2522
	public function NewObj4($class, $args){
2523
		$obj = new $class( $args[0], $args[1], $args[2], $args[3] );
2524
		if( $this->CacheEnabled() ){
2525
			$this->ObjCache( $obj, $class, $args);
2526
		}
2527
		return $obj;
2528
	}
2529
2530
	public function NewObj5($class, $args){
2531
		$obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4] );
2532
		if( $this->CacheEnabled() ){
2533
			$this->ObjCache( $obj, $class, $args);
2534
		}
2535
		return $obj;
2536
	}
2537
2538 View Code Duplication
	public function NewObj6($class, $args){
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2539
		$obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5] );
2540
		if( $this->CacheEnabled() ){
2541
			$this->ObjCache( $obj, $class, $args);
2542
		}
2543
		return $obj;
2544
	}
2545
2546 View Code Duplication
	public function NewObj7($class, $args){
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2547
		$obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6] );
2548
		if( $this->CacheEnabled() ){
2549
			$this->ObjCache( $obj, $class, $args);
2550
		}
2551
		return $obj;
2552
	}
2553
2554
	//caching
2555
	public function ObjCache($obj, $class, $args=array()){
2556
		$obj->cache_string = ' new '.$class.'('. self::ArgCache($args).')';
2557
	}
2558
2559
	public function ArgCache($args){
2560
		return implode(',',array_map( array('Less_Parser','ArgString'),$args));
2561
	}
2562
2563
2564
	/**
2565
	 * Convert an argument to a string for use in the parser cache
2566
	 *
2567
	 * @return string
2568
	 */
2569
	public static function ArgString($arg){
2570
2571
		$type = gettype($arg);
2572
2573
		if( $type === 'object'){
2574
			$string = $arg->cache_string;
2575
			unset($arg->cache_string);
2576
			return $string;
2577
2578
		}elseif( $type === 'array' ){
2579
			$string = ' Array(';
2580
			foreach($arg as $k => $a){
2581
				$string .= var_export($k,true).' => '.self::ArgString($a).',';
2582
			}
2583
			return $string . ')';
2584
		}
2585
2586
		return var_export($arg,true);
2587
	}
2588
2589
	public function Error($msg){
2590
		throw new Less_Exception_Parser($msg, null, $this->furthest, $this->env->currentFileInfo);
0 ignored issues
show
Documentation introduced by
$this->env->currentFileInfo is of type array<string,?>, but the function expects a object<Less_FileInfo>|string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2591
	}
2592
2593
	public static function WinPath($path){
2594
		return str_replace('\\', '/', $path);
2595
	}
2596
2597
	public function CacheEnabled(){
2598
		return (Less_Parser::$options['cache_method'] && (Less_Cache::$cache_dir || (Less_Parser::$options['cache_method'] == 'callback')));
2599
	}
2600
2601
}
2602
2603
2604