1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace LesserPhp; |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* lesserphp |
7
|
|
|
* https://www.maswaba.de/lesserphp |
8
|
|
|
* |
9
|
|
|
* LESS CSS compiler, adapted from http://lesscss.org |
10
|
|
|
* |
11
|
|
|
* Copyright 2013, Leaf Corcoran <[email protected]> |
12
|
|
|
* Copyright 2016, Marcus Schwarz <[email protected]> |
13
|
|
|
* Licensed under MIT or GPLv3, see LICENSE |
14
|
|
|
* @package LesserPhp |
15
|
|
|
*/ |
16
|
|
|
use LesserPhp\Color\Converter; |
17
|
|
|
use LesserPhp\Exception\GeneralException; |
18
|
|
|
use LesserPhp\Library\Assertions; |
19
|
|
|
use LesserPhp\Library\Coerce; |
20
|
|
|
use LesserPhp\Library\Functions; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* The LESS compiler and parser. |
24
|
|
|
* |
25
|
|
|
* Converting LESS to CSS is a three stage process. The incoming file is parsed |
26
|
|
|
* by `lessc_parser` into a syntax tree, then it is compiled into another tree |
27
|
|
|
* representing the CSS structure by `lessc`. The CSS tree is fed into a |
28
|
|
|
* formatter, like `lessc_formatter` which then outputs CSS as a string. |
29
|
|
|
* |
30
|
|
|
* During the first compile, all values are *reduced*, which means that their |
31
|
|
|
* types are brought to the lowest form before being dump as strings. This |
32
|
|
|
* handles math equations, variable dereferences, and the like. |
33
|
|
|
* |
34
|
|
|
* The `parse` function of `lessc` is the entry point. |
35
|
|
|
* |
36
|
|
|
* In summary: |
37
|
|
|
* |
38
|
|
|
* The `lessc` class creates an instance of the parser, feeds it LESS code, |
39
|
|
|
* then transforms the resulting tree to a CSS tree. This class also holds the |
40
|
|
|
* evaluation context, such as all available mixins and variables at any given |
41
|
|
|
* time. |
42
|
|
|
* |
43
|
|
|
* The `lessc_parser` class is only concerned with parsing its input. |
44
|
|
|
* |
45
|
|
|
* The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string, |
46
|
|
|
* handling things like indentation. |
47
|
|
|
*/ |
48
|
|
|
class Compiler |
49
|
|
|
{ |
50
|
|
|
|
51
|
|
|
const VERSION = 'v0.5.1'; |
52
|
|
|
|
53
|
|
|
static public $TRUE = ['keyword', 'true']; |
54
|
|
|
static public $FALSE = ['keyword', 'false']; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @var callable[] |
58
|
|
|
*/ |
59
|
|
|
private $libFunctions = []; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @var string[] |
63
|
|
|
*/ |
64
|
|
|
private $registeredVars = []; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @var bool |
68
|
|
|
*/ |
69
|
|
|
protected $preserveComments = false; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @var string $vPrefix prefix of abstract properties |
73
|
|
|
*/ |
74
|
|
|
private $vPrefix = '@'; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* @var string $mPrefix prefix of abstract blocks |
78
|
|
|
*/ |
79
|
|
|
private $mPrefix = '$'; |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* @var string |
83
|
|
|
*/ |
84
|
|
|
private $parentSelector = '&'; |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* @var bool $importDisabled disable @import |
88
|
|
|
*/ |
89
|
|
|
private $importDisabled = false; |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* @var string[] |
93
|
|
|
*/ |
94
|
|
|
private $importDirs = []; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* @var int |
98
|
|
|
*/ |
99
|
|
|
private $numberPrecision; |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* @var string[] |
103
|
|
|
*/ |
104
|
|
|
private $allParsedFiles = []; |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* set to the parser that generated the current line when compiling |
108
|
|
|
* so we know how to create error messages |
109
|
|
|
* @var \LesserPhp\Parser |
110
|
|
|
*/ |
111
|
|
|
private $sourceParser; |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* @var integer $sourceLoc Lines of Code |
115
|
|
|
*/ |
116
|
|
|
private $sourceLoc; |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* @var int $nextImportId uniquely identify imports |
120
|
|
|
*/ |
121
|
|
|
static private $nextImportId = 0; |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* @var \LesserPhp\Parser |
125
|
|
|
*/ |
126
|
|
|
private $parser; |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* @var \LesserPhp\Formatter\FormatterInterface |
130
|
|
|
*/ |
131
|
|
|
private $formatter; |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* @var \LesserPhp\NodeEnv What's the meaning of "env" in this context? |
135
|
|
|
*/ |
136
|
|
|
private $env; |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* @var \LesserPhp\Library\Coerce |
140
|
|
|
*/ |
141
|
|
|
private $coerce; |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* @var \LesserPhp\Library\Assertions |
145
|
|
|
*/ |
146
|
|
|
private $assertions; |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* @var \LesserPhp\Library\Functions |
150
|
|
|
*/ |
151
|
|
|
private $functions; |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* @var mixed what's this exactly? |
155
|
|
|
*/ |
156
|
|
|
private $scope; |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* @var string |
160
|
|
|
*/ |
161
|
|
|
private $formatterName; |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* @var \LesserPhp\Color\Converter |
165
|
|
|
*/ |
166
|
|
|
private $converter; |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* Constructor. |
170
|
|
|
* |
171
|
|
|
* Hardwires dependencies for now |
172
|
|
|
*/ |
173
|
49 |
|
public function __construct() |
174
|
|
|
{ |
175
|
49 |
|
$this->coerce = new Coerce(); |
176
|
49 |
|
$this->assertions = new Assertions($this->coerce); |
177
|
49 |
|
$this->converter = new Converter(); |
178
|
49 |
|
$this->functions = new Functions($this->assertions, $this->coerce, $this, $this->converter); |
179
|
49 |
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* attempts to find the path of an import url, returns null for css files |
183
|
|
|
* |
184
|
|
|
* @param $url |
185
|
|
|
* |
186
|
|
|
* @return null|string |
187
|
|
|
*/ |
188
|
3 |
View Code Duplication |
protected function findImport($url) |
|
|
|
|
189
|
|
|
{ |
190
|
3 |
|
foreach ($this->importDirs as $dir) { |
191
|
3 |
|
$full = $dir . (mb_substr($dir, -1) !== '/' ? '/' : '') . $url; |
192
|
3 |
|
if ($this->fileExists($file = $full . '.less') || $this->fileExists($file = $full)) { |
193
|
3 |
|
return $file; |
194
|
|
|
} |
195
|
|
|
} |
196
|
|
|
|
197
|
2 |
|
return null; |
198
|
|
|
} |
199
|
|
|
|
200
|
3 |
|
protected function fileExists($name) |
201
|
|
|
{ |
202
|
3 |
|
return is_file($name); |
203
|
|
|
} |
204
|
|
|
|
205
|
48 |
|
public static function compressList($items, $delim) |
206
|
|
|
{ |
207
|
48 |
|
if (!isset($items[1]) && isset($items[0])) { |
208
|
48 |
|
return $items[0]; |
209
|
|
|
} else { |
210
|
27 |
|
return ['list', $delim, $items]; |
211
|
|
|
} |
212
|
|
|
} |
213
|
|
|
|
214
|
36 |
|
public static function pregQuote($what) |
215
|
|
|
{ |
216
|
36 |
|
return preg_quote($what, '/'); |
217
|
|
|
} |
218
|
|
|
|
219
|
3 |
|
protected function tryImport($importPath, $parentBlock, $out) |
220
|
|
|
{ |
221
|
3 |
|
if ($importPath[0] === 'function' && $importPath[1] === 'url') { |
222
|
2 |
|
$importPath = $this->flattenList($importPath[2]); |
223
|
|
|
} |
224
|
|
|
|
225
|
3 |
|
$str = $this->coerce->coerceString($importPath); |
226
|
3 |
|
if ($str === null) { |
227
|
2 |
|
return false; |
228
|
|
|
} |
229
|
|
|
|
230
|
3 |
|
$url = $this->compileValue($this->functions->e($str)); |
231
|
|
|
|
232
|
|
|
// don't import if it ends in css |
233
|
3 |
|
if (substr_compare($url, '.css', -4, 4) === 0) { |
234
|
|
|
return false; |
235
|
|
|
} |
236
|
|
|
|
237
|
3 |
|
$realPath = $this->findImport($url); |
238
|
|
|
|
239
|
3 |
|
if ($realPath === null) { |
240
|
2 |
|
return false; |
241
|
|
|
} |
242
|
|
|
|
243
|
3 |
|
if ($this->isImportDisabled()) { |
244
|
1 |
|
return [false, '/* import disabled */']; |
245
|
|
|
} |
246
|
|
|
|
247
|
2 |
|
if (isset($this->allParsedFiles[realpath($realPath)])) { |
248
|
2 |
|
return [false, null]; |
249
|
|
|
} |
250
|
|
|
|
251
|
2 |
|
$this->addParsedFile($realPath); |
252
|
2 |
|
$parser = $this->makeParser($realPath); |
253
|
2 |
|
$root = $parser->parse(file_get_contents($realPath)); |
254
|
|
|
|
255
|
|
|
// set the parents of all the block props |
256
|
2 |
|
foreach ($root->props as $prop) { |
257
|
2 |
|
if ($prop[0] === 'block') { |
258
|
2 |
|
$prop[1]->parent = $parentBlock; |
259
|
|
|
} |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
// copy mixins into scope, set their parents |
263
|
|
|
// bring blocks from import into current block |
264
|
|
|
// TODO: need to mark the source parser these came from this file |
265
|
2 |
|
foreach ($root->children as $childName => $child) { |
266
|
2 |
|
if (isset($parentBlock->children[$childName])) { |
267
|
2 |
|
$parentBlock->children[$childName] = array_merge( |
268
|
2 |
|
$parentBlock->children[$childName], |
269
|
|
|
$child |
270
|
|
|
); |
271
|
|
|
} else { |
272
|
2 |
|
$parentBlock->children[$childName] = $child; |
273
|
|
|
} |
274
|
|
|
} |
275
|
|
|
|
276
|
2 |
|
$pi = pathinfo($realPath); |
277
|
2 |
|
$dir = $pi["dirname"]; |
278
|
|
|
|
279
|
2 |
|
list($top, $bottom) = $this->sortProps($root->props, true); |
280
|
2 |
|
$this->compileImportedProps($top, $parentBlock, $out, $dir); |
281
|
|
|
|
282
|
2 |
|
return [true, $bottom, $parser, $dir]; |
283
|
|
|
} |
284
|
|
|
|
285
|
2 |
|
protected function compileImportedProps($props, $block, $out, $importDir) |
286
|
|
|
{ |
287
|
2 |
|
$oldSourceParser = $this->sourceParser; |
288
|
|
|
|
289
|
2 |
|
$oldImport = $this->importDirs; |
290
|
|
|
|
291
|
2 |
|
array_unshift($this->importDirs, $importDir); |
292
|
|
|
|
293
|
2 |
|
foreach ($props as $prop) { |
294
|
2 |
|
$this->compileProp($prop, $block, $out); |
295
|
|
|
} |
296
|
|
|
|
297
|
2 |
|
$this->importDirs = $oldImport; |
298
|
2 |
|
$this->sourceParser = $oldSourceParser; |
299
|
2 |
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* Recursively compiles a block. |
303
|
|
|
* |
304
|
|
|
* A block is analogous to a CSS block in most cases. A single LESS document |
305
|
|
|
* is encapsulated in a block when parsed, but it does not have parent tags |
306
|
|
|
* so all of it's children appear on the root level when compiled. |
307
|
|
|
* |
308
|
|
|
* Blocks are made up of props and children. |
309
|
|
|
* |
310
|
|
|
* Props are property instructions, array tuples which describe an action |
311
|
|
|
* to be taken, eg. write a property, set a variable, mixin a block. |
312
|
|
|
* |
313
|
|
|
* The children of a block are just all the blocks that are defined within. |
314
|
|
|
* This is used to look up mixins when performing a mixin. |
315
|
|
|
* |
316
|
|
|
* Compiling the block involves pushing a fresh environment on the stack, |
317
|
|
|
* and iterating through the props, compiling each one. |
318
|
|
|
* |
319
|
|
|
* See lessc::compileProp() |
320
|
|
|
* |
321
|
|
|
* @param $block |
322
|
|
|
*/ |
323
|
49 |
|
protected function compileBlock($block) |
324
|
|
|
{ |
325
|
49 |
|
switch ($block->type) { |
326
|
49 |
|
case "root": |
327
|
49 |
|
$this->compileRoot($block); |
328
|
38 |
|
break; |
329
|
46 |
|
case null: |
330
|
46 |
|
$this->compileCSSBlock($block); |
331
|
35 |
|
break; |
332
|
6 |
|
case "media": |
333
|
3 |
|
$this->compileMedia($block); |
334
|
3 |
|
break; |
335
|
4 |
|
case "directive": |
336
|
4 |
|
$name = "@" . $block->name; |
337
|
4 |
|
if (!empty($block->value)) { |
338
|
2 |
|
$name .= " " . $this->compileValue($this->reduce($block->value)); |
339
|
|
|
} |
340
|
|
|
|
341
|
4 |
|
$this->compileNestedBlock($block, [$name]); |
342
|
4 |
|
break; |
343
|
|
|
default: |
344
|
|
|
$block->parser->throwError("unknown block type: $block->type\n", $block->count); |
345
|
|
|
} |
346
|
38 |
|
} |
347
|
|
|
|
348
|
46 |
|
protected function compileCSSBlock($block) |
349
|
|
|
{ |
350
|
46 |
|
$env = $this->pushEnv($this->env); |
351
|
|
|
|
352
|
46 |
|
$selectors = $this->compileSelectors($block->tags); |
353
|
46 |
|
$env->setSelectors($this->multiplySelectors($selectors)); |
354
|
46 |
|
$out = $this->makeOutputBlock(null, $env->getSelectors()); |
355
|
|
|
|
356
|
46 |
|
$this->scope->children[] = $out; |
357
|
46 |
|
$this->compileProps($block, $out); |
358
|
|
|
|
359
|
35 |
|
$block->scope = $env; // mixins carry scope with them! |
360
|
35 |
|
$this->popEnv(); |
361
|
35 |
|
} |
362
|
|
|
|
363
|
3 |
|
protected function compileMedia($media) |
364
|
|
|
{ |
365
|
3 |
|
$env = $this->pushEnv($this->env, $media); |
366
|
3 |
|
$parentScope = $this->mediaParent($this->scope); |
367
|
|
|
|
368
|
3 |
|
$query = $this->compileMediaQuery($this->multiplyMedia($env)); |
369
|
|
|
|
370
|
3 |
|
$this->scope = $this->makeOutputBlock($media->type, [$query]); |
371
|
3 |
|
$parentScope->children[] = $this->scope; |
372
|
|
|
|
373
|
3 |
|
$this->compileProps($media, $this->scope); |
374
|
|
|
|
375
|
3 |
|
if (count($this->scope->lines) > 0) { |
376
|
3 |
|
$orphanSelelectors = $this->findClosestSelectors(); |
|
|
|
|
377
|
3 |
|
if ($orphanSelelectors !== null) { |
378
|
3 |
|
$orphan = $this->makeOutputBlock(null, $orphanSelelectors); |
379
|
3 |
|
$orphan->lines = $this->scope->lines; |
380
|
3 |
|
array_unshift($this->scope->children, $orphan); |
381
|
3 |
|
$this->scope->lines = []; |
382
|
|
|
} |
383
|
|
|
} |
384
|
|
|
|
385
|
3 |
|
$this->scope = $this->scope->parent; |
386
|
3 |
|
$this->popEnv(); |
387
|
3 |
|
} |
388
|
|
|
|
389
|
3 |
|
protected function mediaParent($scope) |
390
|
|
|
{ |
391
|
3 |
|
while (!empty($scope->parent)) { |
392
|
1 |
|
if (!empty($scope->type) && $scope->type !== "media") { |
393
|
1 |
|
break; |
394
|
|
|
} |
395
|
1 |
|
$scope = $scope->parent; |
396
|
|
|
} |
397
|
|
|
|
398
|
3 |
|
return $scope; |
399
|
|
|
} |
400
|
|
|
|
401
|
4 |
|
protected function compileNestedBlock($block, $selectors) |
402
|
|
|
{ |
403
|
4 |
|
$this->pushEnv($this->env, $block); |
404
|
4 |
|
$this->scope = $this->makeOutputBlock($block->type, $selectors); |
405
|
4 |
|
$this->scope->parent->children[] = $this->scope; |
406
|
|
|
|
407
|
4 |
|
$this->compileProps($block, $this->scope); |
408
|
|
|
|
409
|
4 |
|
$this->scope = $this->scope->parent; |
410
|
4 |
|
$this->popEnv(); |
411
|
4 |
|
} |
412
|
|
|
|
413
|
49 |
|
protected function compileRoot($root) |
414
|
|
|
{ |
415
|
49 |
|
$this->pushEnv($this->env); |
416
|
49 |
|
$this->scope = $this->makeOutputBlock($root->type); |
417
|
49 |
|
$this->compileProps($root, $this->scope); |
418
|
38 |
|
$this->popEnv(); |
419
|
38 |
|
} |
420
|
|
|
|
421
|
49 |
|
protected function compileProps($block, $out) |
422
|
|
|
{ |
423
|
49 |
|
foreach ($this->sortProps($block->props) as $prop) { |
424
|
49 |
|
$this->compileProp($prop, $block, $out); |
425
|
|
|
} |
426
|
38 |
|
$out->lines = $this->deduplicate($out->lines); |
427
|
38 |
|
} |
428
|
|
|
|
429
|
|
|
/** |
430
|
|
|
* Deduplicate lines in a block. Comments are not deduplicated. If a |
431
|
|
|
* duplicate rule is detected, the comments immediately preceding each |
432
|
|
|
* occurence are consolidated. |
433
|
|
|
* |
434
|
|
|
* @param array $lines |
435
|
|
|
* |
436
|
|
|
* @return array |
437
|
|
|
*/ |
438
|
38 |
|
protected function deduplicate(array $lines) |
439
|
|
|
{ |
440
|
38 |
|
$unique = []; |
441
|
38 |
|
$comments = []; |
442
|
|
|
|
443
|
38 |
|
foreach ($lines as $line) { |
444
|
38 |
|
if (strpos($line, '/*') === 0) { |
445
|
2 |
|
$comments[] = $line; |
446
|
2 |
|
continue; |
447
|
|
|
} |
448
|
37 |
|
if (!in_array($line, $unique)) { |
449
|
37 |
|
$unique[] = $line; |
450
|
|
|
} |
451
|
37 |
|
array_splice($unique, array_search($line, $unique), 0, $comments); |
452
|
37 |
|
$comments = []; |
453
|
|
|
} |
454
|
|
|
|
455
|
38 |
|
return array_merge($unique, $comments); |
456
|
|
|
} |
457
|
|
|
|
458
|
49 |
|
protected function sortProps(array $props, $split = false) |
459
|
|
|
{ |
460
|
49 |
|
$vars = []; |
461
|
49 |
|
$imports = []; |
462
|
49 |
|
$other = []; |
463
|
49 |
|
$stack = []; |
464
|
|
|
|
465
|
49 |
|
foreach ($props as $prop) { |
466
|
49 |
|
switch ($prop[0]) { |
467
|
49 |
|
case "comment": |
468
|
1 |
|
$stack[] = $prop; |
469
|
1 |
|
break; |
470
|
49 |
|
case "assign": |
471
|
43 |
|
$stack[] = $prop; |
472
|
43 |
|
if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) { |
473
|
23 |
|
$vars = array_merge($vars, $stack); |
474
|
|
|
} else { |
475
|
43 |
|
$other = array_merge($other, $stack); |
476
|
|
|
} |
477
|
43 |
|
$stack = []; |
478
|
43 |
|
break; |
479
|
47 |
|
case "import": |
480
|
3 |
|
$id = self::$nextImportId++; |
481
|
3 |
|
$prop[] = $id; |
482
|
3 |
|
$stack[] = $prop; |
483
|
3 |
|
$imports = array_merge($imports, $stack); |
484
|
3 |
|
$other[] = ["import_mixin", $id]; |
485
|
3 |
|
$stack = []; |
486
|
3 |
|
break; |
487
|
|
|
default: |
488
|
46 |
|
$stack[] = $prop; |
489
|
46 |
|
$other = array_merge($other, $stack); |
490
|
46 |
|
$stack = []; |
491
|
49 |
|
break; |
492
|
|
|
} |
493
|
|
|
} |
494
|
49 |
|
$other = array_merge($other, $stack); |
495
|
|
|
|
496
|
49 |
|
if ($split) { |
497
|
2 |
|
return [array_merge($imports, $vars), $other]; |
498
|
|
|
} else { |
499
|
49 |
|
return array_merge($imports, $vars, $other); |
500
|
|
|
} |
501
|
|
|
} |
502
|
|
|
|
503
|
3 |
|
protected function compileMediaQuery(array $queries) |
504
|
|
|
{ |
505
|
3 |
|
$compiledQueries = []; |
506
|
3 |
|
foreach ($queries as $query) { |
507
|
3 |
|
$parts = []; |
508
|
3 |
|
foreach ($query as $q) { |
509
|
3 |
|
switch ($q[0]) { |
510
|
3 |
|
case "mediaType": |
511
|
3 |
|
$parts[] = implode(" ", array_slice($q, 1)); |
512
|
3 |
|
break; |
513
|
1 |
|
case "mediaExp": |
514
|
1 |
|
if (isset($q[2])) { |
515
|
1 |
|
$parts[] = "($q[1]: " . |
516
|
1 |
|
$this->compileValue($this->reduce($q[2])) . ")"; |
517
|
|
|
} else { |
518
|
1 |
|
$parts[] = "($q[1])"; |
519
|
|
|
} |
520
|
1 |
|
break; |
521
|
1 |
|
case "variable": |
522
|
1 |
|
$parts[] = $this->compileValue($this->reduce($q)); |
523
|
3 |
|
break; |
524
|
|
|
} |
525
|
|
|
} |
526
|
|
|
|
527
|
3 |
|
if (count($parts) > 0) { |
528
|
3 |
|
$compiledQueries[] = implode(" and ", $parts); |
529
|
|
|
} |
530
|
|
|
} |
531
|
|
|
|
532
|
3 |
|
$out = "@media"; |
533
|
3 |
|
if (!empty($parts)) { |
534
|
|
|
$out .= " " . |
535
|
3 |
|
implode($this->formatter->getSelectorSeparator(), $compiledQueries); |
536
|
|
|
} |
537
|
|
|
|
538
|
3 |
|
return $out; |
539
|
|
|
} |
540
|
|
|
|
541
|
3 |
|
protected function multiplyMedia(NodeEnv $env = null, array $childQueries = null) |
542
|
|
|
{ |
543
|
3 |
|
if (is_null($env) || |
544
|
3 |
|
(!empty($env->getBlock()->type) && $env->getBlock()->type !== 'media') |
545
|
|
|
) { |
546
|
3 |
|
return $childQueries; |
547
|
|
|
} |
548
|
|
|
|
549
|
|
|
// plain old block, skip |
550
|
3 |
|
if (empty($env->getBlock()->type)) { |
551
|
3 |
|
return $this->multiplyMedia($env->getParent(), $childQueries); |
552
|
|
|
} |
553
|
|
|
|
554
|
3 |
|
$out = []; |
555
|
3 |
|
$queries = $env->getBlock()->queries; |
556
|
3 |
|
if ($childQueries === null) { |
557
|
3 |
|
$out = $queries; |
558
|
|
|
} else { |
559
|
1 |
|
foreach ($queries as $parent) { |
560
|
1 |
|
foreach ($childQueries as $child) { |
561
|
1 |
|
$out[] = array_merge($parent, $child); |
562
|
|
|
} |
563
|
|
|
} |
564
|
|
|
} |
565
|
|
|
|
566
|
3 |
|
return $this->multiplyMedia($env->getParent(), $out); |
567
|
|
|
} |
568
|
|
|
|
569
|
46 |
|
protected function expandParentSelectors(&$tag, $replace) |
570
|
|
|
{ |
571
|
46 |
|
$parts = explode("$&$", $tag); |
572
|
46 |
|
$count = 0; |
573
|
46 |
|
foreach ($parts as &$part) { |
574
|
46 |
|
$part = str_replace($this->parentSelector, $replace, $part, $c); |
575
|
46 |
|
$count += $c; |
576
|
|
|
} |
577
|
46 |
|
$tag = implode($this->parentSelector, $parts); |
578
|
|
|
|
579
|
46 |
|
return $count; |
580
|
|
|
} |
581
|
|
|
|
582
|
46 |
|
protected function findClosestSelectors() |
583
|
|
|
{ |
584
|
46 |
|
$env = $this->env; |
585
|
46 |
|
$selectors = null; |
586
|
46 |
|
while ($env !== null) { |
587
|
46 |
|
if ($env->getSelectors() !== null) { |
588
|
13 |
|
$selectors = $env->getSelectors(); |
|
|
|
|
589
|
13 |
|
break; |
590
|
|
|
} |
591
|
46 |
|
$env = $env->getParent(); |
592
|
|
|
} |
593
|
|
|
|
594
|
46 |
|
return $selectors; |
595
|
|
|
} |
596
|
|
|
|
597
|
|
|
|
598
|
|
|
// multiply $selectors against the nearest selectors in env |
599
|
46 |
|
protected function multiplySelectors(array $selectors) |
600
|
|
|
{ |
601
|
|
|
// find parent selectors |
602
|
|
|
|
603
|
46 |
|
$parentSelectors = $this->findClosestSelectors(); |
|
|
|
|
604
|
46 |
|
if ($parentSelectors === null) { |
605
|
|
|
// kill parent reference in top level selector |
606
|
46 |
|
foreach ($selectors as &$s) { |
607
|
46 |
|
$this->expandParentSelectors($s, ""); |
608
|
|
|
} |
609
|
|
|
|
610
|
46 |
|
return $selectors; |
611
|
|
|
} |
612
|
|
|
|
613
|
13 |
|
$out = []; |
614
|
13 |
|
foreach ($parentSelectors as $parent) { |
|
|
|
|
615
|
13 |
|
foreach ($selectors as $child) { |
616
|
13 |
|
$count = $this->expandParentSelectors($child, $parent); |
617
|
|
|
|
618
|
|
|
// don't prepend the parent tag if & was used |
619
|
13 |
|
if ($count > 0) { |
620
|
4 |
|
$out[] = trim($child); |
621
|
|
|
} else { |
622
|
13 |
|
$out[] = trim($parent . ' ' . $child); |
623
|
|
|
} |
624
|
|
|
} |
625
|
|
|
} |
626
|
|
|
|
627
|
13 |
|
return $out; |
628
|
|
|
} |
629
|
|
|
|
630
|
|
|
// reduces selector expressions |
631
|
46 |
|
protected function compileSelectors(array $selectors) |
632
|
|
|
{ |
633
|
46 |
|
$out = []; |
634
|
|
|
|
635
|
46 |
|
foreach ($selectors as $s) { |
636
|
46 |
|
if (is_array($s)) { |
637
|
4 |
|
list(, $value) = $s; |
638
|
4 |
|
$out[] = trim($this->compileValue($this->reduce($value))); |
639
|
|
|
} else { |
640
|
46 |
|
$out[] = $s; |
641
|
|
|
} |
642
|
|
|
} |
643
|
|
|
|
644
|
46 |
|
return $out; |
645
|
|
|
} |
646
|
|
|
|
647
|
|
|
/** |
648
|
|
|
* @param $left |
649
|
|
|
* @param $right |
650
|
|
|
* |
651
|
|
|
* @return bool |
652
|
|
|
*/ |
653
|
4 |
|
protected function eq($left, $right) |
654
|
|
|
{ |
655
|
4 |
|
return $left == $right; |
656
|
|
|
} |
657
|
|
|
|
658
|
21 |
|
protected function patternMatch($block, $orderedArgs, $keywordArgs) |
659
|
|
|
{ |
660
|
|
|
// match the guards if it has them |
661
|
|
|
// any one of the groups must have all its guards pass for a match |
662
|
21 |
|
if (!empty($block->guards)) { |
663
|
5 |
|
$groupPassed = false; |
664
|
5 |
|
foreach ($block->guards as $guardGroup) { |
665
|
5 |
|
foreach ($guardGroup as $guard) { |
666
|
5 |
|
$this->pushEnv($this->env); |
667
|
5 |
|
$this->zipSetArgs($block->args, $orderedArgs, $keywordArgs); |
668
|
|
|
|
669
|
5 |
|
$negate = false; |
670
|
5 |
|
if ($guard[0] === "negate") { |
671
|
1 |
|
$guard = $guard[1]; |
672
|
1 |
|
$negate = true; |
673
|
|
|
} |
674
|
|
|
|
675
|
5 |
|
$passed = $this->reduce($guard) == self::$TRUE; |
676
|
5 |
|
if ($negate) { |
677
|
1 |
|
$passed = !$passed; |
678
|
|
|
} |
679
|
|
|
|
680
|
5 |
|
$this->popEnv(); |
681
|
|
|
|
682
|
5 |
|
if ($passed) { |
683
|
3 |
|
$groupPassed = true; |
684
|
|
|
} else { |
685
|
5 |
|
$groupPassed = false; |
686
|
5 |
|
break; |
687
|
|
|
} |
688
|
|
|
} |
689
|
|
|
|
690
|
5 |
|
if ($groupPassed) { |
691
|
5 |
|
break; |
692
|
|
|
} |
693
|
|
|
} |
694
|
|
|
|
695
|
5 |
|
if (!$groupPassed) { |
696
|
5 |
|
return false; |
697
|
|
|
} |
698
|
|
|
} |
699
|
|
|
|
700
|
19 |
|
if (empty($block->args)) { |
701
|
12 |
|
return $block->isVararg || empty($orderedArgs) && empty($keywordArgs); |
702
|
|
|
} |
703
|
|
|
|
704
|
14 |
|
$remainingArgs = $block->args; |
705
|
14 |
|
if ($keywordArgs) { |
706
|
2 |
|
$remainingArgs = []; |
707
|
2 |
|
foreach ($block->args as $arg) { |
708
|
2 |
|
if ($arg[0] === "arg" && isset($keywordArgs[$arg[1]])) { |
709
|
2 |
|
continue; |
710
|
|
|
} |
711
|
|
|
|
712
|
2 |
|
$remainingArgs[] = $arg; |
713
|
|
|
} |
714
|
|
|
} |
715
|
|
|
|
716
|
14 |
|
$i = -1; // no args |
717
|
|
|
// try to match by arity or by argument literal |
718
|
14 |
|
foreach ($remainingArgs as $i => $arg) { |
719
|
14 |
|
switch ($arg[0]) { |
720
|
14 |
|
case "lit": |
721
|
3 |
|
if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) { |
722
|
2 |
|
return false; |
723
|
|
|
} |
724
|
3 |
|
break; |
725
|
14 |
|
case "arg": |
726
|
|
|
// no arg and no default value |
727
|
14 |
|
if (!isset($orderedArgs[$i]) && !isset($arg[2])) { |
728
|
3 |
|
return false; |
729
|
|
|
} |
730
|
14 |
|
break; |
731
|
2 |
|
case "rest": |
732
|
2 |
|
$i--; // rest can be empty |
733
|
14 |
|
break 2; |
734
|
|
|
} |
735
|
|
|
} |
736
|
|
|
|
737
|
13 |
|
if ($block->isVararg) { |
738
|
2 |
|
return true; // not having enough is handled above |
739
|
|
|
} else { |
740
|
13 |
|
$numMatched = $i + 1; |
741
|
|
|
|
742
|
|
|
// greater than because default values always match |
743
|
13 |
|
return $numMatched >= count($orderedArgs); |
744
|
|
|
} |
745
|
|
|
} |
746
|
|
|
|
747
|
21 |
|
protected function patternMatchAll(array $blocks, $orderedArgs, $keywordArgs, array $skip = []) |
748
|
|
|
{ |
749
|
21 |
|
$matches = null; |
750
|
21 |
|
foreach ($blocks as $block) { |
751
|
|
|
// skip seen blocks that don't have arguments |
752
|
21 |
|
if (isset($skip[$block->id]) && !isset($block->args)) { |
753
|
1 |
|
continue; |
754
|
|
|
} |
755
|
|
|
|
756
|
21 |
|
if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) { |
757
|
21 |
|
$matches[] = $block; |
758
|
|
|
} |
759
|
|
|
} |
760
|
|
|
|
761
|
21 |
|
return $matches; |
762
|
|
|
} |
763
|
|
|
|
764
|
|
|
// attempt to find blocks matched by path and args |
765
|
22 |
|
protected function findBlocks($searchIn, array $path, $orderedArgs, $keywordArgs, array $seen = []) |
766
|
|
|
{ |
767
|
22 |
|
if ($searchIn === null) { |
768
|
5 |
|
return null; |
769
|
|
|
} |
770
|
22 |
|
if (isset($seen[$searchIn->id])) { |
771
|
1 |
|
return null; |
772
|
|
|
} |
773
|
22 |
|
$seen[$searchIn->id] = true; |
774
|
|
|
|
775
|
22 |
|
$name = $path[0]; |
776
|
|
|
|
777
|
22 |
|
if (isset($searchIn->children[$name])) { |
778
|
21 |
|
$blocks = $searchIn->children[$name]; |
779
|
21 |
|
if (count($path) === 1) { |
780
|
21 |
|
$matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen); |
|
|
|
|
781
|
21 |
|
if (!empty($matches)) { |
782
|
|
|
// This will return all blocks that match in the closest |
783
|
|
|
// scope that has any matching block, like lessjs |
784
|
21 |
|
return $matches; |
785
|
|
|
} |
786
|
|
|
} else { |
787
|
3 |
|
$matches = []; |
788
|
3 |
|
foreach ($blocks as $subBlock) { |
789
|
3 |
|
$subMatches = $this->findBlocks( |
790
|
|
|
$subBlock, |
791
|
3 |
|
array_slice($path, 1), |
792
|
|
|
$orderedArgs, |
793
|
|
|
$keywordArgs, |
794
|
|
|
$seen |
795
|
|
|
); |
796
|
|
|
|
797
|
3 |
|
if ($subMatches !== null) { |
798
|
3 |
|
foreach ($subMatches as $sm) { |
799
|
3 |
|
$matches[] = $sm; |
800
|
|
|
} |
801
|
|
|
} |
802
|
|
|
} |
803
|
|
|
|
804
|
3 |
|
return count($matches) > 0 ? $matches : null; |
805
|
|
|
} |
806
|
|
|
} |
807
|
22 |
|
if ($searchIn->parent === $searchIn) { |
808
|
|
|
return null; |
809
|
|
|
} |
810
|
|
|
|
811
|
22 |
|
return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen); |
812
|
|
|
} |
813
|
|
|
|
814
|
|
|
// sets all argument names in $args to either the default value |
815
|
|
|
// or the one passed in through $values |
816
|
16 |
|
protected function zipSetArgs(array $args, $orderedValues, $keywordValues) |
817
|
|
|
{ |
818
|
16 |
|
$assignedValues = []; |
819
|
|
|
|
820
|
16 |
|
$i = 0; |
821
|
16 |
|
foreach ($args as $a) { |
822
|
14 |
|
if ($a[0] === "arg") { |
823
|
14 |
|
if (isset($keywordValues[$a[1]])) { |
824
|
|
|
// has keyword arg |
825
|
2 |
|
$value = $keywordValues[$a[1]]; |
826
|
14 |
|
} elseif (isset($orderedValues[$i])) { |
827
|
|
|
// has ordered arg |
828
|
13 |
|
$value = $orderedValues[$i]; |
829
|
13 |
|
$i++; |
830
|
6 |
|
} elseif (isset($a[2])) { |
831
|
|
|
// has default value |
832
|
6 |
|
$value = $a[2]; |
833
|
|
|
} else { |
834
|
|
|
throw new GeneralException("Failed to assign arg " . $a[1]); |
835
|
|
|
} |
836
|
|
|
|
837
|
14 |
|
$value = $this->reduce($value); |
838
|
14 |
|
$this->set($a[1], $value); |
839
|
14 |
|
$assignedValues[] = $value; |
840
|
|
|
} else { |
841
|
|
|
// a lit |
842
|
14 |
|
$i++; |
843
|
|
|
} |
844
|
|
|
} |
845
|
|
|
|
846
|
|
|
// check for a rest |
847
|
16 |
|
$last = end($args); |
848
|
16 |
|
if ($last[0] === "rest") { |
849
|
2 |
|
$rest = array_slice($orderedValues, count($args) - 1); |
850
|
2 |
|
$this->set($last[1], $this->reduce(["list", " ", $rest])); |
851
|
|
|
} |
852
|
|
|
|
853
|
|
|
// wow is this the only true use of PHP's + operator for arrays? |
854
|
16 |
|
$this->env->setArguments($assignedValues + $orderedValues); |
855
|
16 |
|
} |
856
|
|
|
|
857
|
|
|
// compile a prop and update $lines or $blocks appropriately |
858
|
49 |
|
protected function compileProp($prop, $block, $out) |
859
|
|
|
{ |
860
|
|
|
// set error position context |
861
|
49 |
|
$this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1; |
862
|
|
|
|
863
|
49 |
|
switch ($prop[0]) { |
864
|
49 |
|
case 'assign': |
865
|
43 |
|
list(, $name, $value) = $prop; |
866
|
43 |
|
if ($name[0] == $this->vPrefix) { |
867
|
23 |
|
$this->set($name, $value); |
868
|
|
|
} else { |
869
|
43 |
|
$out->lines[] = $this->formatter->property( |
870
|
|
|
$name, |
871
|
43 |
|
$this->compileValue($this->reduce($value)) |
872
|
|
|
); |
873
|
|
|
} |
874
|
37 |
|
break; |
875
|
47 |
|
case 'block': |
876
|
46 |
|
list(, $child) = $prop; |
877
|
46 |
|
$this->compileBlock($child); |
878
|
35 |
|
break; |
879
|
25 |
|
case 'ruleset': |
880
|
25 |
|
case 'mixin': |
881
|
22 |
|
list(, $path, $args, $suffix) = $prop; |
882
|
|
|
|
883
|
22 |
|
$orderedArgs = []; |
884
|
22 |
|
$keywordArgs = []; |
885
|
22 |
|
foreach ((array)$args as $arg) { |
886
|
15 |
|
switch ($arg[0]) { |
887
|
15 |
|
case "arg": |
888
|
4 |
|
if (!isset($arg[2])) { |
889
|
3 |
|
$orderedArgs[] = $this->reduce(["variable", $arg[1]]); |
890
|
|
|
} else { |
891
|
2 |
|
$keywordArgs[$arg[1]] = $this->reduce($arg[2]); |
892
|
|
|
} |
893
|
4 |
|
break; |
894
|
|
|
|
895
|
15 |
|
case "lit": |
896
|
15 |
|
$orderedArgs[] = $this->reduce($arg[1]); |
897
|
15 |
|
break; |
898
|
|
|
default: |
899
|
15 |
|
throw new GeneralException("Unknown arg type: " . $arg[0]); |
900
|
|
|
} |
901
|
|
|
} |
902
|
|
|
|
903
|
22 |
|
$mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs); |
904
|
|
|
|
905
|
22 |
|
if ($mixins === null) { |
906
|
5 |
|
$block->parser->throwError("{$prop[1][0]} is undefined", $block->count); |
907
|
|
|
} |
908
|
|
|
|
909
|
17 |
|
if (strpos($prop[1][0], "$") === 0) { |
910
|
|
|
//Use Ruleset Logic - Only last element |
911
|
8 |
|
$mixins = [array_pop($mixins)]; |
912
|
|
|
} |
913
|
|
|
|
914
|
17 |
|
foreach ($mixins as $mixin) { |
915
|
17 |
|
if ($mixin === $block && !$orderedArgs) { |
|
|
|
|
916
|
|
|
continue; |
917
|
|
|
} |
918
|
|
|
|
919
|
17 |
|
$haveScope = false; |
920
|
17 |
|
if (isset($mixin->parent->scope)) { |
921
|
2 |
|
$haveScope = true; |
922
|
2 |
|
$mixinParentEnv = $this->pushEnv($this->env); |
923
|
2 |
|
$mixinParentEnv->storeParent = $mixin->parent->scope; |
|
|
|
|
924
|
|
|
} |
925
|
|
|
|
926
|
17 |
|
$haveArgs = false; |
927
|
17 |
|
if (isset($mixin->args)) { |
928
|
14 |
|
$haveArgs = true; |
929
|
14 |
|
$this->pushEnv($this->env); |
930
|
14 |
|
$this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs); |
931
|
|
|
} |
932
|
|
|
|
933
|
17 |
|
$oldParent = $mixin->parent; |
934
|
17 |
|
if ($mixin != $block) { |
935
|
17 |
|
$mixin->parent = $block; |
936
|
|
|
} |
937
|
|
|
|
938
|
17 |
|
foreach ($this->sortProps($mixin->props) as $subProp) { |
939
|
17 |
|
if ($suffix !== null && |
940
|
17 |
|
$subProp[0] === "assign" && |
941
|
17 |
|
is_string($subProp[1]) && |
942
|
17 |
|
$subProp[1]{0} != $this->vPrefix |
943
|
|
|
) { |
944
|
1 |
|
$subProp[2] = [ |
945
|
1 |
|
'list', |
946
|
1 |
|
' ', |
947
|
1 |
|
[$subProp[2], ['keyword', $suffix]], |
948
|
|
|
]; |
949
|
|
|
} |
950
|
|
|
|
951
|
17 |
|
$this->compileProp($subProp, $mixin, $out); |
952
|
|
|
} |
953
|
|
|
|
954
|
17 |
|
$mixin->parent = $oldParent; |
955
|
|
|
|
956
|
17 |
|
if ($haveArgs) { |
957
|
14 |
|
$this->popEnv(); |
958
|
|
|
} |
959
|
17 |
|
if ($haveScope) { |
960
|
17 |
|
$this->popEnv(); |
961
|
|
|
} |
962
|
|
|
} |
963
|
|
|
|
964
|
17 |
|
break; |
965
|
5 |
|
case 'raw': |
966
|
|
|
$out->lines[] = $prop[1]; |
967
|
|
|
break; |
968
|
5 |
|
case "directive": |
969
|
1 |
|
list(, $name, $value) = $prop; |
970
|
1 |
|
$out->lines[] = "@$name " . $this->compileValue($this->reduce($value)) . ';'; |
971
|
1 |
|
break; |
972
|
4 |
|
case "comment": |
973
|
1 |
|
$out->lines[] = $prop[1]; |
974
|
1 |
|
break; |
975
|
3 |
|
case "import": |
976
|
3 |
|
list(, $importPath, $importId) = $prop; |
977
|
3 |
|
$importPath = $this->reduce($importPath); |
978
|
|
|
|
979
|
3 |
|
$result = $this->tryImport($importPath, $block, $out); |
980
|
|
|
|
981
|
3 |
|
$this->env->addImports($importId, $result === false ? |
982
|
2 |
|
[false, "@import " . $this->compileValue($importPath) . ";"] : |
983
|
3 |
|
$result); |
984
|
|
|
|
985
|
3 |
|
break; |
986
|
3 |
|
case "import_mixin": |
987
|
3 |
|
list(, $importId) = $prop; |
988
|
3 |
|
$import = $this->env->getImports($importId); |
989
|
3 |
|
if ($import[0] === false) { |
990
|
3 |
|
if (isset($import[1])) { |
991
|
3 |
|
$out->lines[] = $import[1]; |
992
|
|
|
} |
993
|
|
|
} else { |
994
|
2 |
|
list(, $bottom, $parser, $importDir) = $import; |
|
|
|
|
995
|
2 |
|
$this->compileImportedProps($bottom, $block, $out, $importDir); |
996
|
|
|
} |
997
|
|
|
|
998
|
3 |
|
break; |
999
|
|
|
default: |
1000
|
|
|
$block->parser->throwError("unknown op: {$prop[0]}\n", $block->count); |
1001
|
|
|
} |
1002
|
38 |
|
} |
1003
|
|
|
|
1004
|
|
|
|
1005
|
|
|
/** |
1006
|
|
|
* Compiles a primitive value into a CSS property value. |
1007
|
|
|
* |
1008
|
|
|
* Values in lessphp are typed by being wrapped in arrays, their format is |
1009
|
|
|
* typically: |
1010
|
|
|
* |
1011
|
|
|
* array(type, contents [, additional_contents]*) |
1012
|
|
|
* |
1013
|
|
|
* The input is expected to be reduced. This function will not work on |
1014
|
|
|
* things like expressions and variables. |
1015
|
|
|
* |
1016
|
|
|
* @param array $value |
1017
|
|
|
* |
1018
|
|
|
* @return string |
1019
|
|
|
* @throws \LesserPhp\Exception\GeneralException |
1020
|
|
|
*/ |
1021
|
38 |
|
public function compileValue(array $value) |
1022
|
|
|
{ |
1023
|
38 |
|
switch ($value[0]) { |
1024
|
38 |
|
case 'list': |
1025
|
|
|
// [1] - delimiter |
|
|
|
|
1026
|
|
|
// [2] - array of values |
1027
|
21 |
|
return implode($value[1], array_map([$this, 'compileValue'], $value[2])); |
1028
|
38 |
|
case 'raw_color': |
1029
|
8 |
|
if ($this->formatter->getCompressColors()) { |
1030
|
|
|
return $this->compileValue($this->coerce->coerceColor($value)); |
1031
|
|
|
} |
1032
|
|
|
|
1033
|
8 |
|
return $value[1]; |
1034
|
38 |
|
case 'keyword': |
1035
|
|
|
// [1] - the keyword |
1036
|
32 |
|
return $value[1]; |
1037
|
36 |
|
case 'number': |
1038
|
32 |
|
list(, $num, $unit) = $value; |
1039
|
|
|
// [1] - the number |
1040
|
|
|
// [2] - the unit |
1041
|
32 |
|
if ($this->numberPrecision !== null) { |
1042
|
|
|
$num = round($num, $this->numberPrecision); |
1043
|
|
|
} |
1044
|
|
|
|
1045
|
32 |
|
return $num . $unit; |
1046
|
29 |
|
case 'string': |
1047
|
|
|
// [1] - contents of string (includes quotes) |
1048
|
24 |
|
list(, $delim, $content) = $value; |
1049
|
24 |
|
foreach ($content as &$part) { |
1050
|
24 |
|
if (is_array($part)) { |
1051
|
24 |
|
$part = $this->compileValue($part); |
1052
|
|
|
} |
1053
|
|
|
} |
1054
|
|
|
|
1055
|
24 |
|
return $delim . implode($content) . $delim; |
1056
|
17 |
|
case 'color': |
1057
|
|
|
// [1] - red component (either number or a %) |
1058
|
|
|
// [2] - green component |
1059
|
|
|
// [3] - blue component |
1060
|
|
|
// [4] - optional alpha component |
1061
|
8 |
|
list(, $r, $g, $b) = $value; |
1062
|
8 |
|
$r = round($r); |
1063
|
8 |
|
$g = round($g); |
1064
|
8 |
|
$b = round($b); |
1065
|
|
|
|
1066
|
8 |
|
if (count($value) === 5 && $value[4] != 1) { // rgba |
1067
|
3 |
|
return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . $value[4] . ')'; |
1068
|
|
|
} |
1069
|
|
|
|
1070
|
8 |
|
$h = sprintf("#%02x%02x%02x", $r, $g, $b); |
1071
|
|
|
|
1072
|
8 |
|
if ($this->formatter->getCompressColors()) { |
1073
|
|
|
// Converting hex color to short notation (e.g. #003399 to #039) |
1074
|
1 |
|
if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { |
1075
|
1 |
|
$h = '#' . $h[1] . $h[3] . $h[5]; |
1076
|
|
|
} |
1077
|
|
|
} |
1078
|
|
|
|
1079
|
8 |
|
return $h; |
1080
|
|
|
|
1081
|
10 |
|
case 'function': |
1082
|
10 |
|
list(, $name, $args) = $value; |
1083
|
|
|
|
1084
|
10 |
|
return $name . '(' . $this->compileValue($args) . ')'; |
1085
|
|
|
default: // assumed to be unit |
1086
|
|
|
throw new GeneralException('unknown value type: ' . $value[0]); |
1087
|
|
|
} |
1088
|
|
|
} |
1089
|
|
|
|
1090
|
|
|
/** |
1091
|
|
|
* Helper function to get arguments for color manipulation functions. |
1092
|
|
|
* takes a list that contains a color like thing and a percentage |
1093
|
|
|
* |
1094
|
|
|
* @param array $args |
1095
|
|
|
* |
1096
|
|
|
* @return array |
1097
|
|
|
*/ |
1098
|
2 |
|
public function colorArgs(array $args) |
1099
|
|
|
{ |
1100
|
2 |
|
if ($args[0] !== 'list' || count($args[2]) < 2) { |
1101
|
1 |
|
return [['color', 0, 0, 0], 0]; |
1102
|
|
|
} |
1103
|
2 |
|
list($color, $delta) = $args[2]; |
1104
|
2 |
|
$color = $this->assertions->assertColor($color); |
1105
|
2 |
|
$delta = (float)$delta[1]; |
1106
|
|
|
|
1107
|
2 |
|
return [$color, $delta]; |
1108
|
|
|
} |
1109
|
|
|
|
1110
|
|
|
/** |
1111
|
|
|
* Convert the rgb, rgba, hsl color literals of function type |
1112
|
|
|
* as returned by the parser into values of color type. |
1113
|
|
|
* |
1114
|
|
|
* @param array $func |
1115
|
|
|
* |
1116
|
|
|
* @return bool|mixed |
1117
|
|
|
*/ |
1118
|
24 |
|
protected function funcToColor(array $func) |
1119
|
|
|
{ |
1120
|
24 |
|
$fname = $func[1]; |
1121
|
24 |
|
if ($func[2][0] !== 'list') { |
1122
|
6 |
|
return false; |
1123
|
|
|
} // need a list of arguments |
1124
|
|
|
/** @var array $rawComponents */ |
1125
|
24 |
|
$rawComponents = $func[2][2]; |
1126
|
|
|
|
1127
|
24 |
|
if ($fname === 'hsl' || $fname === 'hsla') { |
1128
|
1 |
|
$hsl = ['hsl']; |
1129
|
1 |
|
$i = 0; |
1130
|
1 |
|
foreach ($rawComponents as $c) { |
1131
|
1 |
|
$val = $this->reduce($c); |
1132
|
1 |
|
$val = isset($val[1]) ? (float)$val[1] : 0; |
1133
|
|
|
|
1134
|
1 |
|
if ($i === 0) { |
1135
|
1 |
|
$clamp = 360; |
1136
|
1 |
|
} elseif ($i < 3) { |
1137
|
1 |
|
$clamp = 100; |
1138
|
|
|
} else { |
1139
|
1 |
|
$clamp = 1; |
1140
|
|
|
} |
1141
|
|
|
|
1142
|
1 |
|
$hsl[] = $this->converter->clamp($val, $clamp); |
1143
|
1 |
|
$i++; |
1144
|
|
|
} |
1145
|
|
|
|
1146
|
1 |
|
while (count($hsl) < 4) { |
1147
|
|
|
$hsl[] = 0; |
1148
|
|
|
} |
1149
|
|
|
|
1150
|
1 |
|
return $this->converter->toRGB($hsl); |
1151
|
|
|
|
1152
|
24 |
|
} elseif ($fname === 'rgb' || $fname === 'rgba') { |
1153
|
4 |
|
$components = []; |
1154
|
4 |
|
$i = 1; |
1155
|
4 |
|
foreach ($rawComponents as $c) { |
1156
|
4 |
|
$c = $this->reduce($c); |
1157
|
4 |
|
if ($i < 4) { |
1158
|
4 |
View Code Duplication |
if ($c[0] === "number" && $c[2] === "%") { |
|
|
|
|
1159
|
1 |
|
$components[] = 255 * ($c[1] / 100); |
1160
|
|
|
} else { |
1161
|
4 |
|
$components[] = (float)$c[1]; |
1162
|
|
|
} |
1163
|
4 |
View Code Duplication |
} elseif ($i === 4) { |
|
|
|
|
1164
|
4 |
|
if ($c[0] === "number" && $c[2] === "%") { |
1165
|
|
|
$components[] = 1.0 * ($c[1] / 100); |
1166
|
|
|
} else { |
1167
|
4 |
|
$components[] = (float)$c[1]; |
1168
|
|
|
} |
1169
|
|
|
} else { |
1170
|
|
|
break; |
1171
|
|
|
} |
1172
|
|
|
|
1173
|
4 |
|
$i++; |
1174
|
|
|
} |
1175
|
4 |
|
while (count($components) < 3) { |
1176
|
|
|
$components[] = 0; |
1177
|
|
|
} |
1178
|
4 |
|
array_unshift($components, 'color'); |
1179
|
|
|
|
1180
|
4 |
|
return $this->fixColor($components); |
1181
|
|
|
} |
1182
|
|
|
|
1183
|
23 |
|
return false; |
1184
|
|
|
} |
1185
|
|
|
|
1186
|
|
|
/** |
1187
|
|
|
* @param array $value |
1188
|
|
|
* @param bool $forExpression |
1189
|
|
|
* |
1190
|
|
|
* @return array|bool|mixed|null // <!-- dafuq? |
1191
|
|
|
*/ |
1192
|
48 |
|
public function reduce(array $value, $forExpression = false) |
1193
|
|
|
{ |
1194
|
48 |
|
switch ($value[0]) { |
1195
|
48 |
|
case "interpolate": |
1196
|
7 |
|
$reduced = $this->reduce($value[1]); |
1197
|
7 |
|
$var = $this->compileValue($reduced); |
1198
|
7 |
|
$res = $this->reduce(["variable", $this->vPrefix . $var]); |
1199
|
|
|
|
1200
|
7 |
|
if ($res[0] === "raw_color") { |
1201
|
1 |
|
$res = $this->coerce->coerceColor($res); |
1202
|
|
|
} |
1203
|
|
|
|
1204
|
7 |
|
if (empty($value[2])) { |
1205
|
6 |
|
$res = $this->functions->e($res); |
1206
|
|
|
} |
1207
|
|
|
|
1208
|
7 |
|
return $res; |
1209
|
48 |
|
case "variable": |
1210
|
28 |
|
$key = $value[1]; |
1211
|
28 |
|
if (is_array($key)) { |
1212
|
1 |
|
$key = $this->reduce($key); |
1213
|
1 |
|
$key = $this->vPrefix . $this->compileValue($this->functions->e($key)); |
1214
|
|
|
} |
1215
|
|
|
|
1216
|
28 |
|
$seen =& $this->env->seenNames; |
1217
|
|
|
|
1218
|
28 |
|
if (!empty($seen[$key])) { |
1219
|
|
|
$this->throwError("infinite loop detected: $key"); |
1220
|
|
|
} |
1221
|
|
|
|
1222
|
28 |
|
$seen[$key] = true; |
1223
|
28 |
|
$out = $this->reduce($this->get($key)); |
1224
|
27 |
|
$seen[$key] = false; |
1225
|
|
|
|
1226
|
27 |
|
return $out; |
1227
|
47 |
|
case "list": |
1228
|
29 |
|
foreach ($value[2] as &$item) { |
1229
|
26 |
|
$item = $this->reduce($item, $forExpression); |
1230
|
|
|
} |
1231
|
|
|
|
1232
|
29 |
|
return $value; |
1233
|
47 |
|
case "expression": |
1234
|
19 |
|
return $this->evaluate($value); |
1235
|
47 |
|
case "string": |
1236
|
26 |
|
foreach ($value[2] as &$part) { |
1237
|
26 |
|
if (is_array($part)) { |
1238
|
11 |
|
$strip = $part[0] === "variable"; |
1239
|
11 |
|
$part = $this->reduce($part); |
1240
|
11 |
|
if ($strip) { |
1241
|
26 |
|
$part = $this->functions->e($part); |
1242
|
|
|
} |
1243
|
|
|
} |
1244
|
|
|
} |
1245
|
|
|
|
1246
|
26 |
|
return $value; |
1247
|
45 |
|
case "escape": |
1248
|
4 |
|
list(, $inner) = $value; |
1249
|
|
|
|
1250
|
4 |
|
return $this->functions->e($this->reduce($inner)); |
1251
|
45 |
|
case "function": |
1252
|
24 |
|
$color = $this->funcToColor($value); |
1253
|
24 |
|
if ($color) { |
1254
|
4 |
|
return $color; |
1255
|
|
|
} |
1256
|
|
|
|
1257
|
23 |
|
list(, $name, $args) = $value; |
1258
|
23 |
|
if ($name === "%") { |
1259
|
1 |
|
$name = "_sprintf"; |
1260
|
|
|
} |
1261
|
|
|
|
1262
|
|
|
// user functions |
1263
|
23 |
|
$f = null; |
1264
|
23 |
|
if (isset($this->libFunctions[$name]) && is_callable($this->libFunctions[$name])) { |
1265
|
1 |
|
$f = $this->libFunctions[$name]; |
1266
|
|
|
} |
1267
|
|
|
|
1268
|
23 |
|
$func = str_replace('-', '_', $name); |
1269
|
|
|
|
1270
|
23 |
|
if ($f !== null || method_exists($this->functions, $func)) { |
1271
|
14 |
|
if ($args[0] === 'list') { |
1272
|
14 |
|
$args = self::compressList($args[2], $args[1]); |
1273
|
|
|
} |
1274
|
|
|
|
1275
|
14 |
|
if ($f !== null) { |
1276
|
1 |
|
$ret = $f($this->reduce($args, true), $this); |
1277
|
|
|
} else { |
1278
|
13 |
|
$ret = $this->functions->$func($this->reduce($args, true), $this); |
1279
|
|
|
} |
1280
|
9 |
|
if ($ret === null) { |
1281
|
|
|
return [ |
1282
|
2 |
|
"string", |
1283
|
2 |
|
"", |
1284
|
|
|
[ |
1285
|
2 |
|
$name, |
1286
|
2 |
|
"(", |
1287
|
2 |
|
$args, |
1288
|
2 |
|
")", |
1289
|
|
|
], |
1290
|
|
|
]; |
1291
|
|
|
} |
1292
|
|
|
|
1293
|
|
|
// convert to a typed value if the result is a php primitive |
1294
|
8 |
|
if (is_numeric($ret)) { |
1295
|
3 |
|
$ret = ['number', $ret, ""]; |
1296
|
7 |
|
} elseif (!is_array($ret)) { |
1297
|
2 |
|
$ret = ['keyword', $ret]; |
1298
|
|
|
} |
1299
|
|
|
|
1300
|
8 |
|
return $ret; |
1301
|
|
|
} |
1302
|
|
|
|
1303
|
|
|
// plain function, reduce args |
1304
|
10 |
|
$value[2] = $this->reduce($value[2]); |
1305
|
|
|
|
1306
|
10 |
|
return $value; |
1307
|
40 |
|
case "unary": |
1308
|
6 |
|
list(, $op, $exp) = $value; |
1309
|
6 |
|
$exp = $this->reduce($exp); |
1310
|
|
|
|
1311
|
6 |
|
if ($exp[0] === "number") { |
1312
|
|
|
switch ($op) { |
1313
|
6 |
|
case "+": |
1314
|
|
|
return $exp; |
1315
|
6 |
|
case "-": |
1316
|
6 |
|
$exp[1] *= -1; |
1317
|
|
|
|
1318
|
6 |
|
return $exp; |
1319
|
|
|
} |
1320
|
|
|
} |
1321
|
|
|
|
1322
|
|
|
return ["string", "", [$op, $exp]]; |
1323
|
|
|
} |
1324
|
|
|
|
1325
|
40 |
|
if ($forExpression) { |
1326
|
22 |
|
switch ($value[0]) { |
1327
|
22 |
|
case "keyword": |
1328
|
5 |
|
$color = $this->coerce->coerceColor($value); |
1329
|
5 |
|
if ($color !== null) { |
1330
|
2 |
|
return $color; |
1331
|
|
|
} |
1332
|
5 |
|
break; |
1333
|
22 |
|
case "raw_color": |
1334
|
6 |
|
return $this->coerce->coerceColor($value); |
1335
|
|
|
} |
1336
|
|
|
} |
1337
|
|
|
|
1338
|
40 |
|
return $value; |
1339
|
|
|
} |
1340
|
|
|
|
1341
|
|
|
|
1342
|
|
|
// turn list of length 1 into value type |
1343
|
2 |
|
protected function flattenList($value) |
1344
|
|
|
{ |
1345
|
2 |
|
if ($value[0] === "list" && count($value[2]) === 1) { |
1346
|
2 |
|
return $this->flattenList($value[2][0]); |
1347
|
|
|
} |
1348
|
|
|
|
1349
|
2 |
|
return $value; |
1350
|
|
|
} |
1351
|
|
|
|
1352
|
5 |
|
public function toBool($a) |
1353
|
|
|
{ |
1354
|
5 |
|
if ($a) { |
1355
|
4 |
|
return self::$TRUE; |
1356
|
|
|
} else { |
1357
|
5 |
|
return self::$FALSE; |
1358
|
|
|
} |
1359
|
|
|
} |
1360
|
|
|
|
1361
|
|
|
// evaluate an expression |
1362
|
19 |
|
protected function evaluate($exp) |
1363
|
|
|
{ |
1364
|
19 |
|
list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp; |
1365
|
|
|
|
1366
|
19 |
|
$left = $this->reduce($left, true); |
1367
|
19 |
|
$right = $this->reduce($right, true); |
1368
|
|
|
|
1369
|
19 |
|
$leftColor = $this->coerce->coerceColor($left); |
1370
|
19 |
|
if ($leftColor !== null) { |
1371
|
5 |
|
$left = $leftColor; |
1372
|
|
|
} |
1373
|
|
|
|
1374
|
19 |
|
$rightColor = $this->coerce->coerceColor($right); |
1375
|
19 |
|
if ($rightColor !== null) { |
1376
|
5 |
|
$right = $rightColor; |
1377
|
|
|
} |
1378
|
|
|
|
1379
|
19 |
|
$ltype = $left[0]; |
1380
|
19 |
|
$rtype = $right[0]; |
1381
|
|
|
|
1382
|
|
|
// operators that work on all types |
1383
|
19 |
|
if ($op === "and") { |
1384
|
|
|
return $this->toBool($left == self::$TRUE && $right == self::$TRUE); |
1385
|
|
|
} |
1386
|
|
|
|
1387
|
19 |
|
if ($op === "=") { |
1388
|
1 |
|
return $this->toBool($this->eq($left, $right)); |
1389
|
|
|
} |
1390
|
|
|
|
1391
|
19 |
|
$str = $this->stringConcatenate($left, $right); |
1392
|
19 |
|
if ($op === "+" && $str !== null) { |
1393
|
2 |
|
return $str; |
1394
|
|
|
} |
1395
|
|
|
|
1396
|
|
|
// type based operators |
1397
|
17 |
|
$fname = "op_${ltype}_${rtype}"; |
1398
|
17 |
|
if (is_callable([$this, $fname])) { |
1399
|
17 |
|
$out = $this->$fname($op, $left, $right); |
1400
|
17 |
|
if ($out !== null) { |
1401
|
17 |
|
return $out; |
1402
|
|
|
} |
1403
|
|
|
} |
1404
|
|
|
|
1405
|
|
|
// make the expression look it did before being parsed |
1406
|
1 |
|
$paddedOp = $op; |
1407
|
1 |
|
if ($whiteBefore) { |
1408
|
1 |
|
$paddedOp = " " . $paddedOp; |
1409
|
|
|
} |
1410
|
1 |
|
if ($whiteAfter) { |
1411
|
1 |
|
$paddedOp .= " "; |
1412
|
|
|
} |
1413
|
|
|
|
1414
|
1 |
|
return ["string", "", [$left, $paddedOp, $right]]; |
1415
|
|
|
} |
1416
|
|
|
|
1417
|
|
|
protected function stringConcatenate($left, $right) |
1418
|
|
|
{ |
1419
|
19 |
|
$strLeft = $this->coerce->coerceString($left); |
1420
|
19 |
|
if ($strLeft !== null) { |
1421
|
2 |
|
if ($right[0] === "string") { |
1422
|
2 |
|
$right[1] = ""; |
1423
|
|
|
} |
1424
|
2 |
|
$strLeft[2][] = $right; |
1425
|
|
|
|
1426
|
2 |
|
return $strLeft; |
1427
|
|
|
} |
1428
|
|
|
|
1429
|
18 |
|
$strRight = $this->coerce->coerceString($right); |
1430
|
18 |
|
if ($strRight !== null) { |
1431
|
1 |
|
array_unshift($strRight[2], $left); |
1432
|
|
|
|
1433
|
1 |
|
return $strRight; |
1434
|
|
|
} |
1435
|
|
|
|
1436
|
17 |
|
return null; |
1437
|
|
|
} |
1438
|
|
|
|
1439
|
|
|
|
1440
|
|
|
// make sure a color's components don't go out of bounds |
1441
|
|
|
public function fixColor($c) |
1442
|
|
|
{ |
1443
|
7 |
|
foreach (range(1, 3) as $i) { |
1444
|
7 |
|
if ($c[$i] < 0) { |
1445
|
|
|
$c[$i] = 0; |
1446
|
|
|
} |
1447
|
7 |
|
if ($c[$i] > 255) { |
1448
|
7 |
|
$c[$i] = 255; |
1449
|
|
|
} |
1450
|
|
|
} |
1451
|
|
|
|
1452
|
7 |
|
return $c; |
1453
|
|
|
} |
1454
|
|
|
|
1455
|
|
|
protected function op_number_color($op, $lft, $rgt) |
1456
|
|
|
{ |
1457
|
1 |
|
if ($op === '+' || $op === '*') { |
1458
|
1 |
|
return $this->op_color_number($op, $rgt, $lft); |
1459
|
|
|
} |
1460
|
|
|
|
1461
|
1 |
|
return null; |
1462
|
|
|
} |
1463
|
|
|
|
1464
|
|
|
protected function op_color_number($op, $lft, $rgt) |
1465
|
|
|
{ |
1466
|
2 |
|
if ($rgt[0] === '%') { |
1467
|
|
|
$rgt[1] /= 100; |
1468
|
|
|
} |
1469
|
|
|
|
1470
|
2 |
|
return $this->op_color_color( |
1471
|
|
|
$op, |
1472
|
|
|
$lft, |
1473
|
2 |
|
array_fill(1, count($lft) - 1, $rgt[1]) |
1474
|
|
|
); |
1475
|
|
|
} |
1476
|
|
|
|
1477
|
|
|
protected function op_color_color($op, $left, $right) |
1478
|
|
|
{ |
1479
|
5 |
|
$out = ['color']; |
1480
|
5 |
|
$max = count($left) > count($right) ? count($left) : count($right); |
1481
|
5 |
|
foreach (range(1, $max - 1) as $i) { |
1482
|
5 |
|
$lval = isset($left[$i]) ? $left[$i] : 0; |
1483
|
5 |
|
$rval = isset($right[$i]) ? $right[$i] : 0; |
1484
|
|
|
switch ($op) { |
1485
|
5 |
|
case '+': |
1486
|
5 |
|
$out[] = $lval + $rval; |
1487
|
5 |
|
break; |
1488
|
1 |
|
case '-': |
1489
|
1 |
|
$out[] = $lval - $rval; |
1490
|
1 |
|
break; |
1491
|
1 |
|
case '*': |
1492
|
1 |
|
$out[] = $lval * $rval; |
1493
|
1 |
|
break; |
1494
|
1 |
|
case '%': |
1495
|
1 |
|
$out[] = $lval % $rval; |
1496
|
1 |
|
break; |
1497
|
1 |
|
case '/': |
1498
|
1 |
|
if ($rval == 0) { |
1499
|
|
|
throw new GeneralException("evaluate error: can't divide by zero"); |
1500
|
|
|
} |
1501
|
1 |
|
$out[] = $lval / $rval; |
1502
|
1 |
|
break; |
1503
|
|
|
default: |
1504
|
5 |
|
throw new GeneralException('evaluate error: color op number failed on op ' . $op); |
1505
|
|
|
} |
1506
|
|
|
} |
1507
|
|
|
|
1508
|
5 |
|
return $this->fixColor($out); |
1509
|
|
|
} |
1510
|
|
|
|
1511
|
|
|
|
1512
|
|
|
// operator on two numbers |
1513
|
|
|
protected function op_number_number($op, $left, $right) |
1514
|
|
|
{ |
1515
|
15 |
|
$unit = empty($left[2]) ? $right[2] : $left[2]; |
1516
|
|
|
|
1517
|
15 |
|
$value = 0; |
|
|
|
|
1518
|
|
|
switch ($op) { |
1519
|
15 |
|
case '+': |
1520
|
9 |
|
$value = $left[1] + $right[1]; |
1521
|
9 |
|
break; |
1522
|
12 |
|
case '*': |
1523
|
8 |
|
$value = $left[1] * $right[1]; |
1524
|
8 |
|
break; |
1525
|
10 |
|
case '-': |
1526
|
6 |
|
$value = $left[1] - $right[1]; |
1527
|
6 |
|
break; |
1528
|
9 |
|
case '%': |
1529
|
1 |
|
$value = $left[1] % $right[1]; |
1530
|
1 |
|
break; |
1531
|
8 |
|
case '/': |
1532
|
4 |
|
if ($right[1] == 0) { |
1533
|
|
|
throw new GeneralException('parse error: divide by zero'); |
1534
|
|
|
} |
1535
|
4 |
|
$value = $left[1] / $right[1]; |
1536
|
4 |
|
break; |
1537
|
5 |
|
case '<': |
1538
|
1 |
|
return $this->toBool($left[1] < $right[1]); |
1539
|
5 |
|
case '>': |
1540
|
3 |
|
return $this->toBool($left[1] > $right[1]); |
1541
|
3 |
|
case '>=': |
1542
|
1 |
|
return $this->toBool($left[1] >= $right[1]); |
1543
|
2 |
|
case '=<': |
1544
|
2 |
|
return $this->toBool($left[1] <= $right[1]); |
1545
|
|
|
default: |
1546
|
|
|
throw new GeneralException('parse error: unknown number operator: ' . $op); |
1547
|
|
|
} |
1548
|
|
|
|
1549
|
13 |
|
return ["number", $value, $unit]; |
1550
|
|
|
} |
1551
|
|
|
|
1552
|
|
|
|
1553
|
|
|
/* environment functions */ |
1554
|
|
|
|
1555
|
|
|
protected function makeOutputBlock($type, $selectors = null) |
1556
|
|
|
{ |
1557
|
49 |
|
$b = new \stdClass(); |
1558
|
49 |
|
$b->lines = []; |
1559
|
49 |
|
$b->children = []; |
1560
|
49 |
|
$b->selectors = $selectors; |
1561
|
49 |
|
$b->type = $type; |
1562
|
49 |
|
$b->parent = $this->scope; |
1563
|
|
|
|
1564
|
49 |
|
return $b; |
1565
|
|
|
} |
1566
|
|
|
|
1567
|
|
|
// the state of execution |
1568
|
|
|
protected function pushEnv($parent, $block = null) |
1569
|
|
|
{ |
1570
|
49 |
|
$e = new \LesserPhp\NodeEnv(); |
1571
|
49 |
|
$e->setParent($parent); |
1572
|
49 |
|
$e->setBlock($block); |
1573
|
49 |
|
$e->setStore([]); |
1574
|
|
|
|
1575
|
49 |
|
$this->env = $e; |
1576
|
|
|
|
1577
|
49 |
|
return $e; |
1578
|
|
|
} |
1579
|
|
|
|
1580
|
|
|
// pop something off the stack |
1581
|
|
|
protected function popEnv() |
1582
|
|
|
{ |
1583
|
40 |
|
$old = $this->env; |
1584
|
40 |
|
$this->env = $this->env->getParent(); |
1585
|
|
|
|
1586
|
40 |
|
return $old; |
1587
|
|
|
} |
1588
|
|
|
|
1589
|
|
|
// set something in the current env |
1590
|
|
|
protected function set($name, $value) |
1591
|
|
|
{ |
1592
|
27 |
|
$this->env->addStore($name, $value); |
1593
|
27 |
|
} |
1594
|
|
|
|
1595
|
|
|
|
1596
|
|
|
// get the highest occurrence entry for a name |
1597
|
|
|
protected function get($name) |
1598
|
|
|
{ |
1599
|
28 |
|
$current = $this->env; |
1600
|
|
|
|
1601
|
|
|
// track scope to evaluate |
1602
|
28 |
|
$scope_secondary = []; |
1603
|
|
|
|
1604
|
28 |
|
$isArguments = $name === $this->vPrefix . 'arguments'; |
1605
|
28 |
|
while ($current) { |
1606
|
28 |
|
if ($isArguments && count($current->getArguments()) > 0) { |
1607
|
3 |
|
return ['list', ' ', $current->getArguments()]; |
1608
|
|
|
} |
1609
|
|
|
|
1610
|
28 |
|
if (isset($current->getStore()[$name])) { |
1611
|
27 |
|
return $current->getStore()[$name]; |
1612
|
|
|
} |
1613
|
|
|
// has secondary scope? |
1614
|
19 |
|
if (isset($current->storeParent)) { |
1615
|
|
|
$scope_secondary[] = $current->storeParent; |
|
|
|
|
1616
|
|
|
} |
1617
|
|
|
|
1618
|
19 |
|
if ($current->getParent() !== null) { |
1619
|
19 |
|
$current = $current->getParent(); |
1620
|
|
|
} else { |
1621
|
1 |
|
$current = null; |
1622
|
|
|
} |
1623
|
|
|
} |
1624
|
|
|
|
1625
|
1 |
|
while (count($scope_secondary)) { |
1626
|
|
|
// pop one off |
1627
|
|
|
$current = array_shift($scope_secondary); |
1628
|
|
|
while ($current) { |
1629
|
|
|
if ($isArguments && isset($current->arguments)) { |
1630
|
|
|
return ['list', ' ', $current->arguments]; |
1631
|
|
|
} |
1632
|
|
|
|
1633
|
|
|
if (isset($current->store[$name])) { |
1634
|
|
|
return $current->store[$name]; |
1635
|
|
|
} |
1636
|
|
|
|
1637
|
|
|
// has secondary scope? |
1638
|
|
|
if (isset($current->storeParent)) { |
1639
|
|
|
$scope_secondary[] = $current->storeParent; |
1640
|
|
|
} |
1641
|
|
|
|
1642
|
|
|
if (isset($current->parent)) { |
1643
|
|
|
$current = $current->parent; |
1644
|
|
|
} else { |
1645
|
|
|
$current = null; |
1646
|
|
|
} |
1647
|
|
|
} |
1648
|
|
|
} |
1649
|
|
|
|
1650
|
1 |
|
throw new GeneralException("variable $name is undefined"); |
1651
|
|
|
} |
1652
|
|
|
|
1653
|
|
|
// inject array of unparsed strings into environment as variables |
1654
|
|
|
protected function injectVariables(array $args) |
1655
|
|
|
{ |
1656
|
1 |
|
$this->pushEnv($this->env); |
1657
|
1 |
|
$parser = new \LesserPhp\Parser($this, __METHOD__); |
1658
|
1 |
|
foreach ($args as $name => $strValue) { |
1659
|
1 |
|
if ($name{0} !== '@') { |
1660
|
1 |
|
$name = '@' . $name; |
1661
|
|
|
} |
1662
|
1 |
|
$parser->count = 0; |
1663
|
1 |
|
$parser->buffer = (string)$strValue; |
1664
|
1 |
|
if (!$parser->propertyValue($value)) { |
1665
|
|
|
throw new GeneralException("failed to parse passed in variable $name: $strValue"); |
1666
|
|
|
} |
1667
|
|
|
|
1668
|
1 |
|
$this->set($name, $value); |
1669
|
|
|
} |
1670
|
1 |
|
} |
1671
|
|
|
|
1672
|
|
|
/** |
1673
|
|
|
* @param string $string |
1674
|
|
|
* @param string $name |
1675
|
|
|
* |
1676
|
|
|
* @return string |
1677
|
|
|
*/ |
1678
|
|
|
public function compile($string, $name = null) |
1679
|
|
|
{ |
1680
|
49 |
|
$locale = setlocale(LC_NUMERIC, 0); |
1681
|
49 |
|
setlocale(LC_NUMERIC, 'C'); |
1682
|
|
|
|
1683
|
49 |
|
$this->parser = $this->makeParser($name); |
1684
|
49 |
|
$root = $this->parser->parse($string); |
1685
|
|
|
|
1686
|
49 |
|
$this->env = null; |
1687
|
49 |
|
$this->scope = null; |
1688
|
49 |
|
$this->allParsedFiles = []; |
1689
|
|
|
|
1690
|
49 |
|
$this->formatter = $this->newFormatter(); |
1691
|
|
|
|
1692
|
49 |
|
if (!empty($this->registeredVars)) { |
1693
|
1 |
|
$this->injectVariables($this->registeredVars); |
1694
|
|
|
} |
1695
|
|
|
|
1696
|
49 |
|
$this->sourceParser = $this->parser; // used for error messages |
1697
|
49 |
|
$this->compileBlock($root); |
1698
|
|
|
|
1699
|
38 |
|
ob_start(); |
1700
|
38 |
|
$this->formatter->block($this->scope); |
1701
|
38 |
|
$out = ob_get_clean(); |
1702
|
38 |
|
setlocale(LC_NUMERIC, $locale); |
1703
|
|
|
|
1704
|
38 |
|
return $out; |
1705
|
|
|
} |
1706
|
|
|
|
1707
|
|
|
public function compileFile($fname, $outFname = null) |
1708
|
|
|
{ |
1709
|
1 |
|
if (!is_readable($fname)) { |
1710
|
|
|
throw new GeneralException('load error: failed to find ' . $fname); |
1711
|
|
|
} |
1712
|
|
|
|
1713
|
1 |
|
$pi = pathinfo($fname); |
1714
|
|
|
|
1715
|
1 |
|
$oldImport = $this->importDirs; |
1716
|
|
|
|
1717
|
1 |
|
$this->importDirs[] = $pi['dirname'] . '/'; |
1718
|
|
|
|
1719
|
1 |
|
$this->addParsedFile($fname); |
1720
|
|
|
|
1721
|
1 |
|
$out = $this->compile(file_get_contents($fname), $fname); |
1722
|
|
|
|
1723
|
1 |
|
$this->importDirs = $oldImport; |
1724
|
|
|
|
1725
|
1 |
|
if ($outFname !== null) { |
1726
|
|
|
return file_put_contents($outFname, $out); |
1727
|
|
|
} |
1728
|
|
|
|
1729
|
1 |
|
return $out; |
1730
|
|
|
} |
1731
|
|
|
|
1732
|
|
|
/** |
1733
|
|
|
* Based on explicit input/output files does a full change check on cache before compiling. |
1734
|
|
|
* |
1735
|
|
|
* @param string $in |
1736
|
|
|
* @param string $out |
1737
|
|
|
* @param boolean $force |
1738
|
|
|
* |
1739
|
|
|
* @return string Compiled CSS results |
1740
|
|
|
* @throws \Exception |
1741
|
|
|
*/ |
1742
|
|
|
public function checkedCachedCompile($in, $out, $force = false) |
1743
|
|
|
{ |
1744
|
1 |
|
if (!is_file($in) || !is_readable($in)) { |
1745
|
|
|
throw new GeneralException('Invalid or unreadable input file specified.'); |
1746
|
|
|
} |
1747
|
1 |
|
if (is_dir($out) || !is_writable(file_exists($out) ? $out : dirname($out))) { |
1748
|
|
|
throw new GeneralException('Invalid or unwritable output file specified.'); |
1749
|
|
|
} |
1750
|
|
|
|
1751
|
1 |
|
$outMeta = $out . '.meta'; |
1752
|
1 |
|
$metadata = null; |
1753
|
1 |
|
if (!$force && is_file($outMeta)) { |
1754
|
|
|
$metadata = unserialize(file_get_contents($outMeta)); |
1755
|
|
|
} |
1756
|
|
|
|
1757
|
1 |
|
$output = $this->cachedCompile($metadata ?: $in); |
1758
|
|
|
|
1759
|
1 |
|
if (!$metadata || $metadata['updated'] != $output['updated']) { |
1760
|
1 |
|
$css = $output['compiled']; |
1761
|
1 |
|
unset($output['compiled']); |
1762
|
1 |
|
file_put_contents($out, $css); |
1763
|
1 |
|
file_put_contents($outMeta, serialize($output)); |
1764
|
|
|
} else { |
1765
|
|
|
$css = file_get_contents($out); |
1766
|
|
|
} |
1767
|
|
|
|
1768
|
1 |
|
return $css; |
1769
|
|
|
} |
1770
|
|
|
|
1771
|
|
|
// compile only if changed input has changed or output doesn't exist |
1772
|
|
|
public function checkedCompile($in, $out) |
1773
|
|
|
{ |
1774
|
|
|
if (!is_file($out) || filemtime($in) > filemtime($out)) { |
1775
|
|
|
$this->compileFile($in, $out); |
1776
|
|
|
|
1777
|
|
|
return true; |
1778
|
|
|
} |
1779
|
|
|
|
1780
|
|
|
return false; |
1781
|
|
|
} |
1782
|
|
|
|
1783
|
|
|
/** |
1784
|
|
|
* Execute lessphp on a .less file or a lessphp cache structure |
1785
|
|
|
* |
1786
|
|
|
* The lessphp cache structure contains information about a specific |
1787
|
|
|
* less file having been parsed. It can be used as a hint for future |
1788
|
|
|
* calls to determine whether or not a rebuild is required. |
1789
|
|
|
* |
1790
|
|
|
* The cache structure contains two important keys that may be used |
1791
|
|
|
* externally: |
1792
|
|
|
* |
1793
|
|
|
* compiled: The final compiled CSS |
1794
|
|
|
* updated: The time (in seconds) the CSS was last compiled |
1795
|
|
|
* |
1796
|
|
|
* The cache structure is a plain-ol' PHP associative array and can |
1797
|
|
|
* be serialized and unserialized without a hitch. |
1798
|
|
|
* |
1799
|
|
|
* @param mixed $in Input |
1800
|
|
|
* @param bool $force Force rebuild? |
1801
|
|
|
* |
1802
|
|
|
* @return array lessphp cache structure |
1803
|
|
|
*/ |
1804
|
|
|
public function cachedCompile($in, $force = false) |
1805
|
|
|
{ |
1806
|
|
|
// assume no root |
1807
|
1 |
|
$root = null; |
1808
|
|
|
|
1809
|
1 |
|
if (is_string($in)) { |
1810
|
1 |
|
$root = $in; |
1811
|
|
|
} elseif (is_array($in) && isset($in['root'])) { |
1812
|
|
|
if ($force || !isset($in['files'])) { |
1813
|
|
|
// If we are forcing a recompile or if for some reason the |
1814
|
|
|
// structure does not contain any file information we should |
1815
|
|
|
// specify the root to trigger a rebuild. |
1816
|
|
|
$root = $in['root']; |
1817
|
|
|
} elseif (isset($in['files']) && is_array($in['files'])) { |
1818
|
|
|
foreach ($in['files'] as $fname => $ftime) { |
1819
|
|
|
if (!file_exists($fname) || filemtime($fname) > $ftime) { |
1820
|
|
|
// One of the files we knew about previously has changed |
1821
|
|
|
// so we should look at our incoming root again. |
1822
|
|
|
$root = $in['root']; |
1823
|
|
|
break; |
1824
|
|
|
} |
1825
|
|
|
} |
1826
|
|
|
} |
1827
|
|
|
} else { |
1828
|
|
|
// TODO: Throw an exception? We got neither a string nor something |
1829
|
|
|
// that looks like a compatible lessphp cache structure. |
1830
|
|
|
return null; |
1831
|
|
|
} |
1832
|
|
|
|
1833
|
1 |
|
if ($root !== null) { |
1834
|
|
|
// If we have a root value which means we should rebuild. |
1835
|
1 |
|
$out = []; |
1836
|
1 |
|
$out['root'] = $root; |
1837
|
1 |
|
$out['compiled'] = $this->compileFile($root); |
1838
|
1 |
|
$out['files'] = $this->allParsedFiles; |
1839
|
1 |
|
$out['updated'] = time(); |
1840
|
|
|
|
1841
|
1 |
|
return $out; |
1842
|
|
|
} else { |
1843
|
|
|
// No changes, pass back the structure |
1844
|
|
|
// we were given initially. |
1845
|
|
|
return $in; |
1846
|
|
|
} |
1847
|
|
|
} |
1848
|
|
|
|
1849
|
|
|
// parse and compile buffer |
1850
|
|
|
// This is deprecated |
1851
|
|
|
public function parse($str = null, $initialVariables = null) |
1852
|
|
|
{ |
1853
|
37 |
|
if (is_array($str)) { |
1854
|
|
|
$initialVariables = $str; |
1855
|
|
|
$str = null; |
1856
|
|
|
} |
1857
|
|
|
|
1858
|
37 |
|
$oldVars = $this->registeredVars; |
1859
|
37 |
|
if ($initialVariables !== null) { |
1860
|
1 |
|
$this->setVariables($initialVariables); |
1861
|
|
|
} |
1862
|
|
|
|
1863
|
37 |
|
if ($str === null) { |
1864
|
|
|
if (empty($this->_parseFile)) { |
|
|
|
|
1865
|
|
|
throw new GeneralException("nothing to parse"); |
1866
|
|
|
} |
1867
|
|
|
|
1868
|
|
|
$out = $this->compileFile($this->_parseFile); |
1869
|
|
|
} else { |
1870
|
37 |
|
$out = $this->compile($str); |
1871
|
|
|
} |
1872
|
|
|
|
1873
|
37 |
|
$this->registeredVars = $oldVars; |
1874
|
|
|
|
1875
|
37 |
|
return $out; |
1876
|
|
|
} |
1877
|
|
|
|
1878
|
|
|
protected function makeParser($name) |
1879
|
|
|
{ |
1880
|
49 |
|
$parser = new \LesserPhp\Parser($this, $name); |
1881
|
49 |
|
$parser->writeComments = $this->preserveComments; |
|
|
|
|
1882
|
|
|
|
1883
|
49 |
|
return $parser; |
1884
|
|
|
} |
1885
|
|
|
|
1886
|
|
|
public function setFormatter($name) |
1887
|
|
|
{ |
1888
|
1 |
|
$this->formatterName = $name; |
1889
|
1 |
|
} |
1890
|
|
|
|
1891
|
|
|
/** |
1892
|
|
|
* @return \LesserPhp\Formatter\FormatterInterface |
1893
|
|
|
*/ |
1894
|
|
|
protected function newFormatter() |
1895
|
|
|
{ |
1896
|
49 |
|
$className = 'Lessjs'; |
1897
|
49 |
|
if (!empty($this->formatterName)) { |
1898
|
1 |
|
if (!is_string($this->formatterName)) { |
1899
|
|
|
return $this->formatterName; |
1900
|
|
|
} |
1901
|
1 |
|
$className = $this->formatterName; |
1902
|
|
|
} |
1903
|
|
|
|
1904
|
49 |
|
$className = '\LesserPhp\Formatter\\' . $className; |
1905
|
|
|
|
1906
|
49 |
|
return new $className; |
1907
|
|
|
} |
1908
|
|
|
|
1909
|
|
|
/** |
1910
|
|
|
* @param bool $preserve |
1911
|
|
|
*/ |
1912
|
|
|
public function setPreserveComments($preserve) |
1913
|
|
|
{ |
1914
|
1 |
|
$this->preserveComments = $preserve; |
1915
|
1 |
|
} |
1916
|
|
|
|
1917
|
|
|
/** |
1918
|
|
|
* @param string $name |
1919
|
|
|
* @param callable $func |
1920
|
|
|
*/ |
1921
|
|
|
public function registerFunction($name, callable $func) |
1922
|
|
|
{ |
1923
|
1 |
|
$this->libFunctions[$name] = $func; |
1924
|
1 |
|
} |
1925
|
|
|
|
1926
|
|
|
/** |
1927
|
|
|
* @param string $name |
1928
|
|
|
*/ |
1929
|
|
|
public function unregisterFunction($name) |
1930
|
|
|
{ |
1931
|
1 |
|
unset($this->libFunctions[$name]); |
1932
|
1 |
|
} |
1933
|
|
|
|
1934
|
|
|
/** |
1935
|
|
|
* @param array $variables |
1936
|
|
|
*/ |
1937
|
|
|
public function setVariables(array $variables) |
1938
|
|
|
{ |
1939
|
1 |
|
$this->registeredVars = array_merge($this->registeredVars, $variables); |
|
|
|
|
1940
|
1 |
|
} |
1941
|
|
|
|
1942
|
|
|
/** |
1943
|
|
|
* @param $name |
1944
|
|
|
*/ |
1945
|
|
|
public function unsetVariable($name) |
1946
|
|
|
{ |
1947
|
|
|
unset($this->registeredVars[$name]); |
1948
|
|
|
} |
1949
|
|
|
|
1950
|
|
|
/** |
1951
|
|
|
* @param string[] $dirs |
1952
|
|
|
*/ |
1953
|
|
|
public function setImportDirs(array $dirs) |
1954
|
|
|
{ |
1955
|
38 |
|
$this->importDirs = $dirs; |
1956
|
38 |
|
} |
1957
|
|
|
|
1958
|
|
|
/** |
1959
|
|
|
* @param string $dir |
1960
|
|
|
*/ |
1961
|
|
|
public function addImportDir($dir) |
1962
|
|
|
{ |
1963
|
|
|
$this->importDirs[] = $dir; |
1964
|
|
|
} |
1965
|
|
|
|
1966
|
|
|
/** |
1967
|
|
|
* @return string[] |
1968
|
|
|
*/ |
1969
|
|
|
public function getImportDirs() |
1970
|
|
|
{ |
1971
|
1 |
|
return $this->importDirs; |
1972
|
|
|
} |
1973
|
|
|
|
1974
|
|
|
/** |
1975
|
|
|
* @param string $file |
1976
|
|
|
*/ |
1977
|
|
|
public function addParsedFile($file) |
1978
|
|
|
{ |
1979
|
2 |
|
$this->allParsedFiles[realpath($file)] = filemtime($file); |
1980
|
2 |
|
} |
1981
|
|
|
|
1982
|
|
|
/** |
1983
|
|
|
* Uses the current value of $this->count to show line and line number |
1984
|
|
|
* |
1985
|
|
|
* @param string $msg |
1986
|
|
|
* |
1987
|
|
|
* @throws \Exception |
1988
|
|
|
*/ |
1989
|
|
|
public function throwError($msg = null) |
1990
|
|
|
{ |
1991
|
|
|
if ($this->sourceLoc >= 0) { |
1992
|
|
|
$this->sourceParser->throwError($msg, $this->sourceLoc); |
1993
|
|
|
} |
1994
|
|
|
throw new GeneralException($msg); |
1995
|
|
|
} |
1996
|
|
|
|
1997
|
|
|
// compile file $in to file $out if $in is newer than $out |
1998
|
|
|
// returns true when it compiles, false otherwise |
1999
|
|
|
public static function ccompile($in, $out, Compiler $less = null) |
2000
|
|
|
{ |
2001
|
|
|
if ($less === null) { |
2002
|
|
|
$less = new self; |
2003
|
|
|
} |
2004
|
|
|
|
2005
|
|
|
return $less->checkedCompile($in, $out); |
2006
|
|
|
} |
2007
|
|
|
|
2008
|
|
|
public static function cexecute($in, $force = false, Compiler $less = null) |
2009
|
|
|
{ |
2010
|
|
|
if ($less === null) { |
2011
|
|
|
$less = new self; |
2012
|
|
|
} |
2013
|
|
|
|
2014
|
|
|
return $less->cachedCompile($in, $force); |
2015
|
|
|
} |
2016
|
|
|
|
2017
|
|
|
/** |
2018
|
|
|
* prefix of abstract properties |
2019
|
|
|
* |
2020
|
|
|
* @return string |
2021
|
|
|
*/ |
2022
|
|
|
public function getVPrefix() |
2023
|
|
|
{ |
2024
|
49 |
|
return $this->vPrefix; |
2025
|
|
|
} |
2026
|
|
|
|
2027
|
|
|
/** |
2028
|
|
|
* prefix of abstract blocks |
2029
|
|
|
* |
2030
|
|
|
* @return string |
2031
|
|
|
*/ |
2032
|
|
|
public function getMPrefix() |
2033
|
|
|
{ |
2034
|
46 |
|
return $this->mPrefix; |
2035
|
|
|
} |
2036
|
|
|
|
2037
|
|
|
/** |
2038
|
|
|
* @return string |
2039
|
|
|
*/ |
2040
|
|
|
public function getParentSelector() |
2041
|
|
|
{ |
2042
|
3 |
|
return $this->parentSelector; |
2043
|
|
|
} |
2044
|
|
|
|
2045
|
|
|
/** |
2046
|
|
|
* @param int $numberPresicion |
2047
|
|
|
*/ |
2048
|
|
|
protected function setNumberPrecision($numberPresicion = null) |
2049
|
|
|
{ |
2050
|
|
|
$this->numberPrecision = $numberPresicion; |
2051
|
|
|
} |
2052
|
|
|
|
2053
|
|
|
/** |
2054
|
|
|
* @return \LesserPhp\Library\Coerce |
2055
|
|
|
*/ |
2056
|
|
|
protected function getCoerce() |
2057
|
|
|
{ |
2058
|
|
|
return $this->coerce; |
2059
|
|
|
} |
2060
|
|
|
|
2061
|
|
|
public function setImportDisabled() |
2062
|
|
|
{ |
2063
|
1 |
|
$this->importDisabled = true; |
2064
|
1 |
|
} |
2065
|
|
|
|
2066
|
|
|
/** |
2067
|
|
|
* @return bool |
2068
|
|
|
*/ |
2069
|
|
|
public function isImportDisabled() |
2070
|
|
|
{ |
2071
|
3 |
|
return $this->importDisabled; |
2072
|
|
|
} |
2073
|
|
|
} |
2074
|
|
|
|
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.