1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the ILess |
5
|
|
|
* |
6
|
|
|
* For the full copyright and license information, please view the LICENSE |
7
|
|
|
* file that was distributed with this source code. |
8
|
|
|
*/ |
9
|
|
|
|
10
|
|
|
namespace ILess\Parser; |
11
|
|
|
|
12
|
|
|
use ILess\Color; |
13
|
|
|
use ILess\Context; |
14
|
|
|
use ILess\DebugInfo; |
15
|
|
|
use ILess\Exception\CompilerException; |
16
|
|
|
use ILess\Exception\ParserException; |
17
|
|
|
use ILess\ImportedFile; |
18
|
|
|
use ILess\Importer; |
19
|
|
|
use ILess\Node; |
20
|
|
|
use ILess\Node\AlphaNode; |
21
|
|
|
use ILess\Node\AnonymousNode; |
22
|
|
|
use ILess\Node\AssignmentNode; |
23
|
|
|
use ILess\Node\AttributeNode; |
24
|
|
|
use ILess\Node\CallNode; |
25
|
|
|
use ILess\Node\ColorNode; |
26
|
|
|
use ILess\Node\CombinatorNode; |
27
|
|
|
use ILess\Node\CommentNode; |
28
|
|
|
use ILess\Node\ConditionNode; |
29
|
|
|
use ILess\Node\DetachedRulesetNode; |
30
|
|
|
use ILess\Node\DimensionNode; |
31
|
|
|
use ILess\Node\DirectiveNode; |
32
|
|
|
use ILess\Node\ElementNode; |
33
|
|
|
use ILess\Node\ExpressionNode; |
34
|
|
|
use ILess\Node\ExtendNode; |
35
|
|
|
use ILess\Node\ImportNode; |
36
|
|
|
use ILess\Node\JavascriptNode; |
37
|
|
|
use ILess\Node\KeywordNode; |
38
|
|
|
use ILess\Node\MediaNode; |
39
|
|
|
use ILess\Node\MixinCallNode; |
40
|
|
|
use ILess\Node\MixinDefinitionNode; |
41
|
|
|
use ILess\Node\NegativeNode; |
42
|
|
|
use ILess\Node\OperationNode; |
43
|
|
|
use ILess\Node\ParenNode; |
44
|
|
|
use ILess\Node\QuotedNode; |
45
|
|
|
use ILess\Node\RuleNode; |
46
|
|
|
use ILess\Node\RulesetCallNode; |
47
|
|
|
use ILess\Node\RulesetNode; |
48
|
|
|
use ILess\Node\SelectorNode; |
49
|
|
|
use ILess\Node\UnicodeDescriptorNode; |
50
|
|
|
use ILess\Node\UrlNode; |
51
|
|
|
use ILess\Node\ValueNode; |
52
|
|
|
use ILess\Node\VariableNode; |
53
|
|
|
use ILess\Plugin\PostProcessorInterface; |
54
|
|
|
use ILess\Plugin\PreProcessorInterface; |
55
|
|
|
use ILess\PluginManager; |
56
|
|
|
use ILess\SourceMap\Generator; |
57
|
|
|
use ILess\Util; |
58
|
|
|
use ILess\Variable; |
59
|
|
|
use ILess\Visitor\ImportVisitor; |
60
|
|
|
use ILess\Visitor\JoinSelectorVisitor; |
61
|
|
|
use ILess\Visitor\ProcessExtendsVisitor; |
62
|
|
|
use ILess\Visitor\ToCSSVisitor; |
63
|
|
|
use ILess\Visitor\Visitor; |
64
|
|
|
use InvalidArgumentException; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Parser core. |
68
|
|
|
*/ |
69
|
|
|
class Core |
70
|
|
|
{ |
71
|
|
|
/** |
72
|
|
|
* Parser version. |
73
|
|
|
*/ |
74
|
|
|
const VERSION = '2.2.0'; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* Less.js compatibility version. |
78
|
|
|
*/ |
79
|
|
|
const LESS_JS_VERSION = '2.5.x'; |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* The context. |
83
|
|
|
* |
84
|
|
|
* @var Context |
85
|
|
|
*/ |
86
|
|
|
protected $context; |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* The importer. |
90
|
|
|
* |
91
|
|
|
* @var Importer |
92
|
|
|
*/ |
93
|
|
|
protected $importer; |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Array of variables. |
97
|
|
|
* |
98
|
|
|
* @var array |
99
|
|
|
*/ |
100
|
|
|
protected $variables = []; |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* Array of parsed rules. |
104
|
|
|
* |
105
|
|
|
* @var array |
106
|
|
|
*/ |
107
|
|
|
protected $rules = []; |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* @var ParserInput |
111
|
|
|
*/ |
112
|
|
|
protected $input; |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* @var PluginManager|null |
116
|
|
|
*/ |
117
|
|
|
protected $pluginManager; |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* Constructor. |
121
|
|
|
* |
122
|
|
|
* @param Context $context The context |
123
|
|
|
* @param Importer $importer The importer |
124
|
|
|
* @param PluginManager $pluginManager The plugin manager |
125
|
|
|
*/ |
126
|
|
|
public function __construct(Context $context, Importer $importer, PluginManager $pluginManager = null) |
127
|
|
|
{ |
128
|
|
|
$this->context = $context; |
129
|
|
|
$this->importer = $importer; |
130
|
|
|
$this->pluginManager = $pluginManager; |
131
|
|
|
$this->input = new ParserInput(); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* Parse a Less string from a given file. |
136
|
|
|
* |
137
|
|
|
* @throws ParserException |
138
|
|
|
* |
139
|
|
|
* @param string|ImportedFile $file The file to parse (Will be loaded via the importer) |
140
|
|
|
* @param bool $returnRuleset Indicates whether the parsed rules should be wrapped in a ruleset. |
141
|
|
|
* |
142
|
|
|
* @return mixed If $returnRuleset is true, ILess\Parser\Core, ILess\ILess\Node\RulesetNode otherwise |
143
|
|
|
*/ |
144
|
|
|
public function parseFile($file, $returnRuleset = false) |
145
|
|
|
{ |
146
|
|
|
// save the previous information |
147
|
|
|
$previousFileInfo = $this->context->currentFileInfo; |
148
|
|
|
|
149
|
|
|
if (!($file instanceof ImportedFile)) { |
150
|
|
|
$this->context->setCurrentFile($file); |
151
|
|
|
|
152
|
|
|
if ($previousFileInfo) { |
153
|
|
|
$this->context->currentFileInfo->reference = $previousFileInfo->reference; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
// try to load it via importer |
157
|
|
|
list(, $file) = $this->importer->import($file, true, $this->context->currentFileInfo); |
158
|
|
|
|
159
|
|
|
/* @var $file ImportedFile */ |
160
|
|
|
$this->context->setCurrentFile($file->getPath()); |
161
|
|
|
$this->context->currentFileInfo->importedFile = $file; |
162
|
|
|
|
163
|
|
|
$ruleset = $file->getRuleset(); |
164
|
|
|
} else { |
165
|
|
|
$this->context->setCurrentFile($file->getPath()); |
166
|
|
|
|
167
|
|
|
if ($previousFileInfo) { |
168
|
|
|
$this->context->currentFileInfo->reference = $previousFileInfo->reference; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
$this->context->currentFileInfo->importedFile = $file; |
172
|
|
|
|
173
|
|
|
$ruleset = $file->getRuleset(); |
174
|
|
|
if (!$ruleset) { |
175
|
|
|
$file->setRuleset( |
176
|
|
|
($ruleset = new RulesetNode([], $this->parse($file->getContent()))) |
177
|
|
|
); |
178
|
|
|
} |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
if ($previousFileInfo) { |
182
|
|
|
$this->context->currentFileInfo = $previousFileInfo; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
if ($returnRuleset) { |
186
|
|
|
return $ruleset; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
$this->rules = array_merge($this->rules, $ruleset->rules); |
190
|
|
|
|
191
|
|
|
return $this; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Parses a string. |
196
|
|
|
* |
197
|
|
|
* @param string $string The string to parse |
198
|
|
|
* @param string $filename The filename for reference (will be visible in the source map) or path to a fake file which directory will be used for imports |
199
|
|
|
* @param bool $returnRuleset Return the ruleset? |
200
|
|
|
* |
201
|
|
|
* @return $this |
202
|
|
|
*/ |
203
|
|
|
public function parseString($string, $filename = '__string_to_parse__', $returnRuleset = false) |
204
|
|
|
{ |
205
|
|
|
$string = Util::normalizeString((string) $string); |
206
|
|
|
|
207
|
|
|
// we need unique key |
208
|
|
|
$key = sprintf('%s[__%s__]', $filename, md5($string)); |
209
|
|
|
|
210
|
|
|
// create a dummy information, since we are not parsing a real file, |
211
|
|
|
// but a string coming from outside |
212
|
|
|
$this->context->setCurrentFile($filename); |
213
|
|
|
|
214
|
|
|
$importedFile = new ImportedFile($key, $string, time()); |
215
|
|
|
|
216
|
|
|
// save information, so the exceptions can handle errors in the string |
217
|
|
|
// and source map is generated for the string |
218
|
|
|
$this->context->currentFileInfo->importedFile = $importedFile; |
219
|
|
|
$this->importer->setImportedFile($key, $importedFile, $key, $this->context->currentFileInfo); |
220
|
|
|
|
221
|
|
|
if ($this->context->sourceMap) { |
222
|
|
|
$this->context->setFileContent($key, $string); |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
$importedFile->setRuleset( |
226
|
|
|
($ruleset = new RulesetNode([], $this->parse($string))) |
227
|
|
|
); |
228
|
|
|
|
229
|
|
|
if ($returnRuleset) { |
230
|
|
|
return $ruleset; |
|
|
|
|
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
$this->rules = array_merge($this->rules, $ruleset->rules); |
234
|
|
|
|
235
|
|
|
return $this; |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* Adds variables. |
240
|
|
|
* |
241
|
|
|
* @param array $variables Array of variables |
242
|
|
|
* |
243
|
|
|
* @return $this |
244
|
|
|
*/ |
245
|
|
|
public function addVariables(array $variables) |
246
|
|
|
{ |
247
|
|
|
$this->variables = array_merge($this->variables, $variables); |
248
|
|
|
|
249
|
|
|
return $this; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Clears all assigned variables. |
254
|
|
|
* |
255
|
|
|
* @return $this |
256
|
|
|
*/ |
257
|
|
|
public function clearVariables() |
258
|
|
|
{ |
259
|
|
|
$this->variables = []; |
260
|
|
|
|
261
|
|
|
return $this; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* Sets variables. |
266
|
|
|
* |
267
|
|
|
* @param array $variables |
268
|
|
|
* |
269
|
|
|
* @return $this |
270
|
|
|
*/ |
271
|
|
|
public function setVariables(array $variables) |
272
|
|
|
{ |
273
|
|
|
$this->variables = $variables; |
274
|
|
|
|
275
|
|
|
return $this; |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* Unsets a previously set variable. |
280
|
|
|
* |
281
|
|
|
* @param string|array $variable The variable name(s) to unset as string or an array |
282
|
|
|
* |
283
|
|
|
* @see setVariables, addVariables |
284
|
|
|
* |
285
|
|
|
* @return $this |
286
|
|
|
*/ |
287
|
|
|
public function unsetVariable($variable) |
288
|
|
|
{ |
289
|
|
|
if (!is_array($variable)) { |
290
|
|
|
$variable = [$variable]; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
foreach ($variable as $name) { |
294
|
|
|
if (isset($this->variables[$name])) { |
295
|
|
|
unset($this->variables[$name]); |
296
|
|
|
} elseif (isset($this->variables['!' . $name])) { |
297
|
|
|
unset($this->variables['!' . $name]); |
298
|
|
|
} |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
return $this; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
/** |
305
|
|
|
* Parse a Less string into nodes. |
306
|
|
|
* |
307
|
|
|
* @param string $string The string to parse |
308
|
|
|
* |
309
|
|
|
* @return array |
310
|
|
|
* |
311
|
|
|
* @throws ParserException If there was an error in parsing the string |
312
|
|
|
*/ |
313
|
|
|
protected function parse($string) |
314
|
|
|
{ |
315
|
|
|
$string = Util::normalizeString($string); |
316
|
|
|
|
317
|
|
|
if ($this->pluginManager) { |
318
|
|
|
$preProcessors = $this->pluginManager->getPreProcessors(); |
319
|
|
|
foreach ($preProcessors as $preProcessor) { |
320
|
|
|
/* @var $preProcessor PreProcessorInterface */ |
321
|
|
|
$string = $preProcessor->process($string, [ |
322
|
|
|
'context' => $this->context, |
323
|
|
|
'file_info' => $this->context->currentFileInfo, |
324
|
|
|
'importer' => $this->importer, |
325
|
|
|
]); |
326
|
|
|
} |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
$this->input = new ParserInput(); |
330
|
|
|
$this->input->start($string); |
331
|
|
|
$rules = $this->parsePrimary(); |
332
|
|
|
|
333
|
|
|
$endInfo = $this->input->end(); |
334
|
|
|
$error = null; |
335
|
|
|
|
336
|
|
|
if (!$endInfo->isFinished) { |
337
|
|
|
$message = $endInfo->furthestPossibleErrorMessage; |
338
|
|
|
if (!$message) { |
339
|
|
|
$message = 'Unrecognised input'; |
340
|
|
|
if ($endInfo->furthestChar === '}') { |
341
|
|
|
$message .= '. Possibly missing opening \'{\''; |
342
|
|
|
} elseif ($endInfo->furthestChar === ')') { |
343
|
|
|
$message .= '. Possibly missing opening \'(\''; |
344
|
|
|
} elseif ($endInfo->furthestReachedEnd) { |
345
|
|
|
$message .= '. Possibly missing something'; |
346
|
|
|
} |
347
|
|
|
} |
348
|
|
|
$error = new ParserException($message, $endInfo->furthest, $this->context->currentFileInfo); |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
if ($error) { |
352
|
|
|
throw $error; |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
return $rules; |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* Resets the parser. |
360
|
|
|
* |
361
|
|
|
* @param bool $variables Reset also assigned variables via the API? |
362
|
|
|
* |
363
|
|
|
* @return $this |
364
|
|
|
*/ |
365
|
|
|
public function reset($variables = true) |
366
|
|
|
{ |
367
|
|
|
$this->rules = []; |
368
|
|
|
|
369
|
|
|
if ($variables) { |
370
|
|
|
$this->clearVariables(); |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
return $this; |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
* Returns the plugin manager. |
378
|
|
|
* |
379
|
|
|
* @return PluginManager|null |
380
|
|
|
*/ |
381
|
|
|
public function getPluginManager() |
382
|
|
|
{ |
383
|
|
|
return $this->pluginManager; |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
/** |
387
|
|
|
* Generates unique cache key for given $filename. |
388
|
|
|
* |
389
|
|
|
* @param string $filename |
390
|
|
|
* |
391
|
|
|
* @return string |
392
|
|
|
*/ |
393
|
|
|
protected function generateCacheKey($filename) |
394
|
|
|
{ |
395
|
|
|
return Util::generateCacheKey($filename); |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
/** |
399
|
|
|
* Returns the CSS. |
400
|
|
|
* |
401
|
|
|
* @return string |
402
|
|
|
*/ |
403
|
|
|
public function getCSS() |
404
|
|
|
{ |
405
|
|
|
if (!count($this->rules)) { |
406
|
|
|
return ''; |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
return $this->toCSS($this->getRootRuleset(), $this->variables); |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
/** |
413
|
|
|
* Returns root ruleset. |
414
|
|
|
* |
415
|
|
|
* @return RulesetNode |
416
|
|
|
*/ |
417
|
|
|
protected function getRootRuleset() |
418
|
|
|
{ |
419
|
|
|
$root = new RulesetNode([], $this->rules); |
420
|
|
|
$root->root = true; |
421
|
|
|
$root->firstRoot = true; |
422
|
|
|
$root->allowImports = true; |
423
|
|
|
|
424
|
|
|
return $root; |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
/** |
428
|
|
|
* Converts the ruleset to CSS. |
429
|
|
|
* |
430
|
|
|
* @param RulesetNode $ruleset |
431
|
|
|
* @param array $variables |
432
|
|
|
* |
433
|
|
|
* @return string The generated CSS code |
434
|
|
|
* |
435
|
|
|
* @throws |
436
|
|
|
*/ |
437
|
|
|
protected function toCSS(RulesetNode $ruleset, array $variables) |
438
|
|
|
{ |
439
|
|
|
$precision = ini_set('precision', 16); |
440
|
|
|
$locale = setlocale(LC_NUMERIC, 0); |
441
|
|
|
setlocale(LC_NUMERIC, 'C'); |
442
|
|
|
|
443
|
|
|
if (extension_loaded('xdebug')) { |
444
|
|
|
$level = ini_set('xdebug.max_nesting_level', PHP_INT_MAX); |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
$e = $css = null; |
448
|
|
|
try { |
449
|
|
|
$this->prepareVariables($this->context, $variables); |
450
|
|
|
|
451
|
|
|
// pre compilation visitors |
452
|
|
|
foreach ($this->getPreCompileVisitors() as $visitor) { |
453
|
|
|
/* @var $visitor Visitor */ |
454
|
|
|
$visitor->run($ruleset); |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
// compile the ruleset |
458
|
|
|
$compiled = $ruleset->compile($this->context); |
459
|
|
|
|
460
|
|
|
// post compilation visitors |
461
|
|
|
foreach ($this->getPostCompileVisitors() as $visitor) { |
462
|
|
|
/* @var $visitor Visitor */ |
463
|
|
|
$visitor->run($compiled); |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
$context = $this->getContext(); |
467
|
|
|
$context->numPrecision = 8; // less.js compatibility |
468
|
|
|
|
469
|
|
|
if ($context->sourceMap) { |
470
|
|
|
$generator = new Generator( |
471
|
|
|
$compiled, |
472
|
|
|
$this->context->getContentsMap(), $this->context->sourceMapOptions |
473
|
|
|
); |
474
|
|
|
// will also save file |
475
|
|
|
$css = $generator->generateCSS($this->context); |
476
|
|
|
} else { |
477
|
|
|
$generator = null; |
478
|
|
|
$css = $compiled->toCSS($this->context); |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
if ($this->pluginManager) { |
482
|
|
|
// post process |
483
|
|
|
$postProcessors = $this->pluginManager->getPostProcessors(); |
484
|
|
|
foreach ($postProcessors as $postProcessor) { |
485
|
|
|
/* @var $postProcessor PostProcessorInterface */ |
486
|
|
|
$css = $postProcessor->process($css, [ |
487
|
|
|
'context' => $this->context, |
488
|
|
|
'source_map' => $generator, |
489
|
|
|
'importer' => $this->importer, |
490
|
|
|
]); |
491
|
|
|
} |
492
|
|
|
} |
493
|
|
|
|
494
|
|
|
if ($this->context->compress) { |
495
|
|
|
$css = preg_replace('/(^(\s)+)|((\s)+$)/', '', $css); |
496
|
|
|
} |
497
|
|
|
} catch (\Exception $e) { |
498
|
|
|
} |
499
|
|
|
|
500
|
|
|
// restore |
501
|
|
|
setlocale(LC_NUMERIC, $locale); |
502
|
|
|
ini_set('precision', $precision); |
503
|
|
|
|
504
|
|
|
if (extension_loaded('xdebug')) { |
505
|
|
|
ini_set('xdebug.max_nesting_level', $level); |
506
|
|
|
} |
507
|
|
|
|
508
|
|
|
if ($e) { |
509
|
|
|
throw $e; |
510
|
|
|
} |
511
|
|
|
|
512
|
|
|
return $css; |
513
|
|
|
} |
514
|
|
|
|
515
|
|
|
/** |
516
|
|
|
* Prepare variable to be used as nodes. |
517
|
|
|
* |
518
|
|
|
* @param Context $context |
519
|
|
|
* @param array $variables |
520
|
|
|
*/ |
521
|
|
|
protected function prepareVariables(Context $context, array $variables) |
522
|
|
|
{ |
523
|
|
|
// FIXME: flag to mark variables as prepared! |
524
|
|
|
$prepared = []; |
525
|
|
|
foreach ($variables as $name => $value) { |
526
|
|
|
// user provided node, no need to process it further |
527
|
|
|
if ($value instanceof Node) { |
528
|
|
|
$prepared[] = $value; |
529
|
|
|
continue; |
530
|
|
|
} |
531
|
|
|
// this is not an "real" variable |
532
|
|
|
if (!$value instanceof Variable) { |
533
|
|
|
$value = Variable::create($name, $value); |
534
|
|
|
} |
535
|
|
|
$prepared[] = $value->toNode(); |
536
|
|
|
} |
537
|
|
|
|
538
|
|
|
if (count($prepared)) { |
539
|
|
|
$context->customVariables = new RulesetNode([], $prepared); |
540
|
|
|
} |
541
|
|
|
} |
542
|
|
|
|
543
|
|
|
/** |
544
|
|
|
* Returns array of pre compilation visitors. |
545
|
|
|
* |
546
|
|
|
* @return array |
547
|
|
|
*/ |
548
|
|
|
protected function getPreCompileVisitors() |
549
|
|
|
{ |
550
|
|
|
$preCompileVisitors = []; |
551
|
|
|
|
552
|
|
|
if ($this->context->processImports) { |
553
|
|
|
$preCompileVisitors[] = new ImportVisitor($this->getContext(), $this->getImporter()); |
554
|
|
|
} |
555
|
|
|
|
556
|
|
|
if ($this->pluginManager) { |
557
|
|
|
$preCompileVisitors = array_merge( |
558
|
|
|
$preCompileVisitors, |
559
|
|
|
$this->pluginManager->getPreCompileVisitors() |
560
|
|
|
); |
561
|
|
|
} |
562
|
|
|
|
563
|
|
|
return $preCompileVisitors; |
564
|
|
|
} |
565
|
|
|
|
566
|
|
|
/** |
567
|
|
|
* Returns an array of post compilation visitors. |
568
|
|
|
* |
569
|
|
|
* @return array |
570
|
|
|
*/ |
571
|
|
|
protected function getPostCompileVisitors() |
572
|
|
|
{ |
573
|
|
|
// core visitors |
574
|
|
|
$postCompileVisitors = [ |
575
|
|
|
new JoinSelectorVisitor(), |
576
|
|
|
new ProcessExtendsVisitor(), |
577
|
|
|
]; |
578
|
|
|
|
579
|
|
|
if ($this->pluginManager) { |
580
|
|
|
$postCompileVisitors = array_merge( |
581
|
|
|
$this->pluginManager->getPostCompileVisitors(), |
582
|
|
|
$postCompileVisitors |
583
|
|
|
); |
584
|
|
|
} |
585
|
|
|
|
586
|
|
|
$postCompileVisitors[] = new ToCSSVisitor($this->getContext()); |
587
|
|
|
|
588
|
|
|
return $postCompileVisitors; |
589
|
|
|
} |
590
|
|
|
|
591
|
|
|
/** |
592
|
|
|
* @return array |
593
|
|
|
*/ |
594
|
|
|
protected function parsePrimary() |
595
|
|
|
{ |
596
|
|
|
$root = []; |
597
|
|
|
while (true) { |
598
|
|
|
while (true) { |
599
|
|
|
$node = $this->parseComment(); |
600
|
|
|
if (!$node) { |
601
|
|
|
break; |
602
|
|
|
} |
603
|
|
|
$root[] = $node; |
604
|
|
|
} |
605
|
|
|
|
606
|
|
|
if ($this->input->finished) { |
607
|
|
|
break; |
608
|
|
|
} |
609
|
|
|
|
610
|
|
|
if ($this->input->peek('}')) { |
611
|
|
|
break; |
612
|
|
|
} |
613
|
|
|
|
614
|
|
|
$node = $this->parseExtendRule(); |
615
|
|
|
if ($node) { |
616
|
|
|
$root = array_merge($root, $node); |
617
|
|
|
continue; |
618
|
|
|
} |
619
|
|
|
|
620
|
|
|
$node = $this->matchFuncs( |
621
|
|
|
[ |
622
|
|
|
'parseMixinDefinition', |
623
|
|
|
'parseRule', |
624
|
|
|
'parseRuleset', |
625
|
|
|
'parseMixinCall', |
626
|
|
|
'parseRulesetCall', |
627
|
|
|
'parseDirective', |
628
|
|
|
] |
629
|
|
|
); |
630
|
|
|
|
631
|
|
|
if ($node) { |
632
|
|
|
$root[] = $node; |
633
|
|
|
} else { |
634
|
|
|
$foundSemiColon = false; |
635
|
|
|
while ($this->input->char(';')) { |
636
|
|
|
$foundSemiColon = true; |
637
|
|
|
} |
638
|
|
|
if (!$foundSemiColon) { |
639
|
|
|
break; |
640
|
|
|
} |
641
|
|
|
} |
642
|
|
|
} |
643
|
|
|
|
644
|
|
|
return $root; |
645
|
|
|
} |
646
|
|
|
|
647
|
|
|
/** |
648
|
|
|
* comments are collected by the main parsing mechanism and then assigned to nodes |
649
|
|
|
* where the current structure allows it. |
650
|
|
|
* |
651
|
|
|
* @return CommentNode|null |
652
|
|
|
*/ |
653
|
|
|
protected function parseComment() |
654
|
|
|
{ |
655
|
|
|
if (count($this->input->commentStore)) { |
656
|
|
|
$comment = array_shift($this->input->commentStore); |
657
|
|
|
|
658
|
|
|
return new CommentNode( |
659
|
|
|
$comment['text'], |
660
|
|
|
isset($comment['isLineComment']) ? $comment['isLineComment'] : false, |
661
|
|
|
$comment['index'], |
662
|
|
|
$this->context->currentFileInfo |
663
|
|
|
); |
664
|
|
|
} |
665
|
|
|
} |
666
|
|
|
|
667
|
|
|
// The variable part of a variable definition. Used in the `rule` parser |
668
|
|
|
// |
669
|
|
|
// @fink(); |
670
|
|
|
// |
671
|
|
|
protected function parseRulesetCall() |
672
|
|
|
{ |
673
|
|
|
if ($this->input->currentChar() === '@' && ($name = $this->input->re('/\\G(@[\w-]+)\s*\(\s*\)\s*;/'))) { |
674
|
|
|
return new RulesetCallNode($name[1]); |
675
|
|
|
} |
676
|
|
|
} |
677
|
|
|
|
678
|
|
|
/** |
679
|
|
|
* Parses a mixin definition. |
680
|
|
|
* |
681
|
|
|
* @return MixinDefinitionNode|null |
682
|
|
|
*/ |
683
|
|
|
protected function parseMixinDefinition() |
684
|
|
|
{ |
685
|
|
|
if (($this->input->currentChar() !== '.' && $this->input->currentChar() !== '#') || |
686
|
|
|
$this->input->peekReg('/\\G^[^{]*\}/') |
687
|
|
|
) { |
688
|
|
|
return; |
689
|
|
|
} |
690
|
|
|
|
691
|
|
|
$this->input->save(); |
692
|
|
|
|
693
|
|
|
if ($match = $this->input->re('/\\G([#.](?:[\w-]|\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/')) { |
694
|
|
|
$cond = null; |
695
|
|
|
$name = $match[1]; |
696
|
|
|
$argInfo = $this->parseMixinArgs(false); |
697
|
|
|
$params = $argInfo['args']; |
698
|
|
|
$variadic = $argInfo['variadic']; |
699
|
|
|
|
700
|
|
|
// .mixincall("@{a}"); |
701
|
|
|
// looks a bit like a mixin definition.. |
702
|
|
|
// also |
703
|
|
|
// .mixincall(@a: {rule: set;}); |
704
|
|
|
// so we have to be nice and restore |
705
|
|
|
if (!$this->input->char(')')) { |
706
|
|
|
$this->input->restore("Missing closing ')'"); |
707
|
|
|
|
708
|
|
|
return; |
709
|
|
|
} |
710
|
|
|
|
711
|
|
|
$this->input->commentStore = []; |
712
|
|
|
|
713
|
|
|
// Guard |
714
|
|
|
if ($this->input->str('when')) { |
715
|
|
|
$cond = $this->expect('parseConditions', 'Expected conditions'); |
716
|
|
|
} |
717
|
|
|
|
718
|
|
|
$ruleset = $this->parseBlock(); |
719
|
|
|
if (is_array($ruleset)) { |
720
|
|
|
$this->input->forget(); |
721
|
|
|
|
722
|
|
|
return new MixinDefinitionNode($name, $params, $ruleset, $cond, $variadic); |
|
|
|
|
723
|
|
|
} else { |
724
|
|
|
$this->input->restore(); |
725
|
|
|
} |
726
|
|
|
} else { |
727
|
|
|
$this->input->forget(); |
728
|
|
|
} |
729
|
|
|
} |
730
|
|
|
|
731
|
|
|
/** |
732
|
|
|
* Parses a mixin call with an optional argument list. |
733
|
|
|
* |
734
|
|
|
* #mixins > .square(#fff); |
735
|
|
|
* .rounded(4px, black); |
736
|
|
|
* .button; |
737
|
|
|
* |
738
|
|
|
* The `while` loop is there because mixins can be |
739
|
|
|
* namespaced, but we only support the child and descendant |
740
|
|
|
* selector for now. |
741
|
|
|
* |
742
|
|
|
* @return MixinCallNode|null |
743
|
|
|
*/ |
744
|
|
|
protected function parseMixinCall() |
745
|
|
|
{ |
746
|
|
|
$s = $this->input->currentChar(); |
747
|
|
|
$important = false; |
748
|
|
|
$index = $this->input->i; |
749
|
|
|
$c = null; |
750
|
|
|
$args = []; |
751
|
|
|
|
752
|
|
|
if ($s !== '.' && $s !== '#') { |
753
|
|
|
return; |
754
|
|
|
} |
755
|
|
|
|
756
|
|
|
$this->input->save(); // stop us absorbing part of an invalid selector |
757
|
|
|
|
758
|
|
|
$elements = []; |
759
|
|
|
while (true) { |
760
|
|
|
$elemIndex = $this->input->i; |
761
|
|
|
$e = $this->input->re('/\\G[#.](?:[\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/'); |
762
|
|
|
if (!$e) { |
763
|
|
|
break; |
764
|
|
|
} |
765
|
|
|
$elements[] = new ElementNode($c, $e, $elemIndex, $this->context->currentFileInfo); |
|
|
|
|
766
|
|
|
$c = $this->input->char('>'); |
767
|
|
|
} |
768
|
|
|
|
769
|
|
|
if ($elements) { |
770
|
|
|
if ($this->input->char('(')) { |
771
|
|
|
$args = $this->parseMixinArgs(true); |
772
|
|
|
$args = $args['args']; |
773
|
|
|
$this->expect(')'); |
774
|
|
|
} |
775
|
|
|
|
776
|
|
|
if ($this->parseImportant()) { |
777
|
|
|
$important = true; |
778
|
|
|
} |
779
|
|
|
|
780
|
|
|
if ($this->parseEnd()) { |
781
|
|
|
$this->input->forget(); |
782
|
|
|
|
783
|
|
|
return new MixinCallNode($elements, $args, $index, $this->context->currentFileInfo, $important); |
784
|
|
|
} |
785
|
|
|
} |
786
|
|
|
|
787
|
|
|
$this->input->restore(); |
788
|
|
|
} |
789
|
|
|
|
790
|
|
|
/** |
791
|
|
|
* Parses mixin arguments. |
792
|
|
|
* |
793
|
|
|
* @param bool $isCall The definition or function call? |
794
|
|
|
* |
795
|
|
|
* @return array |
796
|
|
|
* |
797
|
|
|
* @throws CompilerException If there is an error the definition of arguments |
798
|
|
|
*/ |
799
|
|
|
protected function parseMixinArgs($isCall) |
800
|
|
|
{ |
801
|
|
|
$expressions = []; |
802
|
|
|
$argsSemiColon = []; |
803
|
|
|
$isSemiColonSeparated = null; |
804
|
|
|
$argsComma = []; |
805
|
|
|
$expressionContainsNamed = null; |
806
|
|
|
$name = null; |
807
|
|
|
$expand = null; |
808
|
|
|
$returner = ['args' => null, 'variadic' => false]; |
809
|
|
|
|
810
|
|
|
$this->input->save(); |
811
|
|
|
|
812
|
|
|
while (true) { |
813
|
|
|
if ($isCall) { |
814
|
|
|
$arg = $this->matchFuncs(['parseDetachedRuleset', 'parseExpression']); |
815
|
|
|
} else { |
816
|
|
|
$this->input->commentStore = []; |
817
|
|
|
if ($this->input->str('...')) { |
818
|
|
|
$returner['variadic'] = true; |
819
|
|
|
if ($this->input->char(';') && !$isSemiColonSeparated) { |
820
|
|
|
$isSemiColonSeparated = true; |
821
|
|
|
} |
822
|
|
|
|
823
|
|
|
if ($isSemiColonSeparated) { |
824
|
|
|
$argsSemiColon[] = ['variadic' => true]; |
825
|
|
|
} else { |
826
|
|
|
$argsComma[] = ['variadic' => true]; |
827
|
|
|
} |
828
|
|
|
break; |
829
|
|
|
} |
830
|
|
|
$arg = $this->matchFuncs( |
831
|
|
|
['parseEntitiesVariable', 'parseEntitiesLiteral', 'parseEntitiesKeyword'] |
832
|
|
|
); |
833
|
|
|
} |
834
|
|
|
|
835
|
|
|
if (!$arg) { |
836
|
|
|
break; |
837
|
|
|
} |
838
|
|
|
|
839
|
|
|
$nameLoop = null; |
840
|
|
|
if ($arg instanceof ExpressionNode) { |
841
|
|
|
$arg->throwAwayComments(); |
842
|
|
|
} |
843
|
|
|
|
844
|
|
|
$value = $arg; |
845
|
|
|
$val = null; |
846
|
|
|
|
847
|
|
|
if ($isCall) { |
848
|
|
|
// ILess\Variable |
849
|
|
|
if (count($arg->value) == 1) { |
850
|
|
|
$val = $arg->value[0]; |
851
|
|
|
} |
852
|
|
|
} else { |
853
|
|
|
$val = $arg; |
854
|
|
|
} |
855
|
|
|
|
856
|
|
|
if ($val instanceof VariableNode) { |
857
|
|
|
if ($this->input->char(':')) { |
858
|
|
|
if (count($expressions) > 0) { |
859
|
|
|
if ($isSemiColonSeparated) { |
860
|
|
|
throw new CompilerException( |
861
|
|
|
'Cannot mix ; and , as delimiter types', |
862
|
|
|
$this->input->i, |
863
|
|
|
$this->context->currentFileInfo |
864
|
|
|
); |
865
|
|
|
} |
866
|
|
|
$expressionContainsNamed = true; |
867
|
|
|
} |
868
|
|
|
|
869
|
|
|
$value = $this->matchFuncs(['parseDetachedRuleset', 'parseExpression']); |
870
|
|
|
if (!$value) { |
871
|
|
|
if ($isCall) { |
872
|
|
|
throw new CompilerException( |
873
|
|
|
'Could not understand value for named argument', |
874
|
|
|
$this->input->i, |
875
|
|
|
$this->context->currentFileInfo |
876
|
|
|
); |
877
|
|
|
} else { |
878
|
|
|
$this->input->restore(); |
879
|
|
|
$returner['args'] = []; |
880
|
|
|
|
881
|
|
|
return $returner; |
882
|
|
|
} |
883
|
|
|
} |
884
|
|
|
|
885
|
|
|
$nameLoop = ($name = $val->name); |
886
|
|
|
} elseif ($this->input->str('...')) { |
887
|
|
|
if (!$isCall) { |
888
|
|
|
$returner['variadic'] = true; |
889
|
|
|
|
890
|
|
|
if ($this->input->char(';') && !$isSemiColonSeparated) { |
891
|
|
|
$isSemiColonSeparated = true; |
892
|
|
|
} |
893
|
|
|
|
894
|
|
|
if ($isSemiColonSeparated) { |
895
|
|
|
$argsSemiColon[] = ['name' => $arg->name, 'variadic' => true]; |
896
|
|
|
} else { |
897
|
|
|
$argsComma[] = ['name' => $arg->name, 'variadic' => true]; |
898
|
|
|
} |
899
|
|
|
break; |
900
|
|
|
} else { |
901
|
|
|
$expand = true; |
902
|
|
|
} |
903
|
|
|
} elseif (!$isCall) { |
904
|
|
|
$name = $nameLoop = $val->name; |
905
|
|
|
$value = null; |
906
|
|
|
} |
907
|
|
|
} |
908
|
|
|
|
909
|
|
|
if ($value) { |
910
|
|
|
$expressions[] = $value; |
911
|
|
|
} |
912
|
|
|
|
913
|
|
|
$argsComma[] = ['name' => $nameLoop, 'value' => $value, 'expand' => $expand]; |
914
|
|
|
|
915
|
|
|
if ($this->input->char(',')) { |
916
|
|
|
continue; |
917
|
|
|
} |
918
|
|
|
|
919
|
|
|
if ($this->input->char(';') || $isSemiColonSeparated) { |
920
|
|
|
if ($expressionContainsNamed) { |
921
|
|
|
throw new CompilerException( |
922
|
|
|
'Cannot mix ; and , as delimiter types', |
923
|
|
|
$this->input->i, |
924
|
|
|
$this->context->currentFileInfo |
925
|
|
|
); |
926
|
|
|
} |
927
|
|
|
|
928
|
|
|
$isSemiColonSeparated = true; |
929
|
|
|
if (count($expressions) > 1) { |
930
|
|
|
$value = new ValueNode($expressions); |
931
|
|
|
} |
932
|
|
|
$argsSemiColon[] = ['name' => $name, 'value' => $value, 'expand' => $expand]; |
933
|
|
|
$name = null; |
934
|
|
|
$expressions = []; |
935
|
|
|
$expressionContainsNamed = false; |
936
|
|
|
} |
937
|
|
|
} |
938
|
|
|
|
939
|
|
|
$this->input->forget(); |
940
|
|
|
$returner['args'] = ($isSemiColonSeparated ? $argsSemiColon : $argsComma); |
941
|
|
|
|
942
|
|
|
return $returner; |
943
|
|
|
} |
944
|
|
|
|
945
|
|
|
/** |
946
|
|
|
* Parses a rule. |
947
|
|
|
* |
948
|
|
|
* @param bool $tryAnonymous |
949
|
|
|
* |
950
|
|
|
* @return RuleNode|null |
951
|
|
|
*/ |
952
|
|
|
protected function parseRule($tryAnonymous = false) |
953
|
|
|
{ |
954
|
|
|
$merge = null; |
955
|
|
|
$startOfRule = $this->input->i; |
956
|
|
|
$value = null; |
957
|
|
|
$merge = null; |
958
|
|
|
$important = null; |
959
|
|
|
$c = $this->input->currentChar(); |
960
|
|
|
|
961
|
|
|
if ($c === '.' || $c === '#' || $c === '&' || $c === ':') { |
962
|
|
|
return; |
963
|
|
|
} |
964
|
|
|
|
965
|
|
|
$this->input->save(); |
966
|
|
|
|
967
|
|
|
if ($name = $this->matchFuncs(['parseVariable', 'parseRuleProperty'])) { |
968
|
|
|
$isVariable = is_string($name); |
969
|
|
|
if ($isVariable) { |
970
|
|
|
$value = $this->parseDetachedRuleset(); |
971
|
|
|
} |
972
|
|
|
|
973
|
|
|
$this->input->commentStore = []; |
974
|
|
|
|
975
|
|
|
if (!$value) { |
976
|
|
|
if (is_array($name) && count($name) > 1) { |
977
|
|
|
$tmp = array_pop($name); |
978
|
|
|
$merge = !$isVariable && $tmp->value ? $tmp->value : false; |
979
|
|
|
} |
980
|
|
|
|
981
|
|
|
$tryValueFirst = !$tryAnonymous && ($this->context->compress || $isVariable); |
982
|
|
|
|
983
|
|
|
if ($tryValueFirst) { |
984
|
|
|
$value = $this->parseValue(); |
985
|
|
|
} |
986
|
|
|
|
987
|
|
|
if (!$value) { |
988
|
|
|
$value = $this->parseAnonymousValue(); |
989
|
|
|
if ($value) { |
990
|
|
|
$this->input->forget(); |
991
|
|
|
|
992
|
|
|
return new RuleNode( |
993
|
|
|
$name, |
994
|
|
|
$value, |
995
|
|
|
false, |
|
|
|
|
996
|
|
|
$merge, |
997
|
|
|
$startOfRule, |
998
|
|
|
$this->context->currentFileInfo |
999
|
|
|
); |
1000
|
|
|
} |
1001
|
|
|
} |
1002
|
|
|
|
1003
|
|
|
if (!$tryValueFirst && !$value) { |
1004
|
|
|
$value = $this->parseValue(); |
1005
|
|
|
} |
1006
|
|
|
|
1007
|
|
|
$important = $this->parseImportant(); |
1008
|
|
|
} |
1009
|
|
|
|
1010
|
|
|
if ($value && $this->parseEnd()) { |
1011
|
|
|
$this->input->forget(); |
1012
|
|
|
|
1013
|
|
|
return new RuleNode( |
1014
|
|
|
$name, $value, $important, $merge, $startOfRule, $this->context->currentFileInfo |
|
|
|
|
1015
|
|
|
); |
1016
|
|
|
} else { |
1017
|
|
|
$this->input->restore(); |
1018
|
|
|
if ($value && !$tryAnonymous) { |
1019
|
|
|
return $this->parseRule(true); |
1020
|
|
|
} |
1021
|
|
|
} |
1022
|
|
|
} |
1023
|
|
|
} |
1024
|
|
|
|
1025
|
|
|
/** |
1026
|
|
|
* Parses an anonymous value. |
1027
|
|
|
* |
1028
|
|
|
* @return AnonymousNode|null |
1029
|
|
|
*/ |
1030
|
|
|
protected function parseAnonymousValue() |
1031
|
|
|
{ |
1032
|
|
|
if ($match = $this->input->re('/\\G([^@+\/\'"*`(;{}-]*);/')) { |
1033
|
|
|
return new AnonymousNode($match[1]); |
1034
|
|
|
} |
1035
|
|
|
} |
1036
|
|
|
|
1037
|
|
|
/** |
1038
|
|
|
* Parses a ruleset like: `div, .class, body > p {...}`. |
1039
|
|
|
* |
1040
|
|
|
* @return RulesetNode|null |
1041
|
|
|
* |
1042
|
|
|
* @throws ParserException |
1043
|
|
|
*/ |
1044
|
|
|
protected function parseRuleset() |
1045
|
|
|
{ |
1046
|
|
|
$selectors = []; |
1047
|
|
|
|
1048
|
|
|
$this->input->save(); |
1049
|
|
|
|
1050
|
|
|
$debugInfo = null; |
1051
|
|
|
if ($this->context->dumpLineNumbers) { |
1052
|
|
|
$debugInfo = $this->getDebugInfo($this->input->i); |
1053
|
|
|
} |
1054
|
|
|
|
1055
|
|
|
while (true) { |
1056
|
|
|
$s = $this->parseLessSelector(); |
1057
|
|
|
if (!$s) { |
1058
|
|
|
break; |
1059
|
|
|
} |
1060
|
|
|
$selectors[] = $s; |
1061
|
|
|
$this->input->commentStore = []; |
1062
|
|
|
if ($s->condition && count($selectors) > 1) { |
1063
|
|
|
throw new ParserException( |
1064
|
|
|
'Guards are only currently allowed on a single selector.', |
1065
|
|
|
$this->input->i, |
1066
|
|
|
$this->context->currentFileInfo |
1067
|
|
|
); |
1068
|
|
|
} |
1069
|
|
|
|
1070
|
|
|
if (!$this->input->char(',')) { |
1071
|
|
|
break; |
1072
|
|
|
} |
1073
|
|
|
|
1074
|
|
|
if ($s->condition) { |
1075
|
|
|
throw new ParserException( |
1076
|
|
|
'Guards are only currently allowed on a single selector.', |
1077
|
|
|
$this->input->i, |
1078
|
|
|
$this->context->currentFileInfo |
1079
|
|
|
); |
1080
|
|
|
} |
1081
|
|
|
|
1082
|
|
|
$this->input->commentStore = []; |
1083
|
|
|
} |
1084
|
|
|
|
1085
|
|
|
if ($selectors && is_array($rules = $this->parseBlock())) { |
1086
|
|
|
$this->input->forget(); |
1087
|
|
|
$ruleset = new RulesetNode($selectors, $rules, $this->context->strictImports); |
1088
|
|
|
if ($debugInfo) { |
1089
|
|
|
$ruleset->debugInfo = $debugInfo; |
1090
|
|
|
} |
1091
|
|
|
|
1092
|
|
|
return $ruleset; |
1093
|
|
|
} else { |
1094
|
|
|
$this->input->restore(); |
1095
|
|
|
} |
1096
|
|
|
} |
1097
|
|
|
|
1098
|
|
|
/** |
1099
|
|
|
* Parses a selector with less extensions e.g. the ability to extend and guard. |
1100
|
|
|
* |
1101
|
|
|
* @return SelectorNode|null |
1102
|
|
|
*/ |
1103
|
|
|
protected function parseLessSelector() |
1104
|
|
|
{ |
1105
|
|
|
return $this->parseSelector(true); |
1106
|
|
|
} |
1107
|
|
|
|
1108
|
|
|
/** |
1109
|
|
|
* Parses a CSS selector. |
1110
|
|
|
* |
1111
|
|
|
* @param bool $isLess Is this a less sector? (ie. has ability to extend and guard) |
1112
|
|
|
* |
1113
|
|
|
* @return SelectorNode|null |
1114
|
|
|
* |
1115
|
|
|
* @throws ParserException |
1116
|
|
|
*/ |
1117
|
|
|
protected function parseSelector($isLess = false) |
1118
|
|
|
{ |
1119
|
|
|
$elements = []; |
1120
|
|
|
$extendList = []; |
1121
|
|
|
$allExtends = []; |
1122
|
|
|
$condition = null; |
1123
|
|
|
$when = false; |
1124
|
|
|
$e = null; |
1125
|
|
|
$c = null; |
1126
|
|
|
$index = $this->input->i; |
1127
|
|
|
|
1128
|
|
|
while (($isLess && ($extendList = $this->parseExtend())) |
1129
|
|
|
|| ($isLess && ($when = $this->input->str('when'))) || ($e = $this->parseElement())) { |
1130
|
|
|
if ($when) { |
1131
|
|
|
$condition = $this->expect('parseConditions', 'Expected condition'); |
1132
|
|
|
} elseif ($condition) { |
1133
|
|
|
throw new ParserException( |
1134
|
|
|
'CSS guard can only be used at the end of selector.', |
1135
|
|
|
$index, |
1136
|
|
|
$this->context->currentFileInfo |
1137
|
|
|
); |
1138
|
|
|
} elseif ($extendList) { |
1139
|
|
|
$allExtends = array_merge($allExtends, $extendList); |
1140
|
|
|
} else { |
1141
|
|
|
if ($allExtends) { |
1142
|
|
|
throw new ParserException( |
1143
|
|
|
'Extend can only be used at the end of selector.', |
1144
|
|
|
$this->input->i, |
1145
|
|
|
$this->context->currentFileInfo |
1146
|
|
|
); |
1147
|
|
|
} |
1148
|
|
|
$c = $this->input->currentChar(); |
1149
|
|
|
$elements[] = $e; |
1150
|
|
|
$e = null; |
1151
|
|
|
} |
1152
|
|
|
|
1153
|
|
|
if ($c === '{' || $c === '}' || $c === ';' || $c === ',' || $c === ')') { |
1154
|
|
|
break; |
1155
|
|
|
} |
1156
|
|
|
} |
1157
|
|
|
|
1158
|
|
|
if ($elements) { |
1159
|
|
|
return new SelectorNode($elements, $allExtends, $condition, $index, $this->context->currentFileInfo); |
|
|
|
|
1160
|
|
|
} |
1161
|
|
|
|
1162
|
|
|
if ($allExtends) { |
1163
|
|
|
throw new ParserException( |
1164
|
|
|
'Extend must be used to extend a selector, it cannot be used on its own', |
1165
|
|
|
$this->input->i, |
1166
|
|
|
$this->context->currentFileInfo |
1167
|
|
|
); |
1168
|
|
|
} |
1169
|
|
|
} |
1170
|
|
|
|
1171
|
|
|
/** |
1172
|
|
|
* Parses extend. |
1173
|
|
|
* |
1174
|
|
|
* @param bool $isRule Is is a rule? |
1175
|
|
|
* |
1176
|
|
|
* @return ExtendNode|null |
1177
|
|
|
* |
1178
|
|
|
* @throws CompilerException |
1179
|
|
|
*/ |
1180
|
|
|
protected function parseExtend($isRule = false) |
1181
|
|
|
{ |
1182
|
|
|
$extendList = []; |
1183
|
|
|
$index = $this->input->i; |
1184
|
|
|
|
1185
|
|
|
if (!$this->input->str($isRule ? '&:extend(' : ':extend(')) { |
1186
|
|
|
return; |
1187
|
|
|
} |
1188
|
|
|
|
1189
|
|
|
do { |
1190
|
|
|
$option = null; |
1191
|
|
|
$elements = []; |
1192
|
|
|
while (!($option = $this->input->re('/\\G(all)(?=\s*(\)|,))/'))) { |
1193
|
|
|
$e = $this->parseElement(); |
1194
|
|
|
if (!$e) { |
1195
|
|
|
break; |
1196
|
|
|
} |
1197
|
|
|
$elements[] = $e; |
1198
|
|
|
} |
1199
|
|
|
|
1200
|
|
|
if ($option) { |
1201
|
|
|
$option = $option[1]; |
1202
|
|
|
} |
1203
|
|
|
|
1204
|
|
|
if (!$elements) { |
1205
|
|
|
throw new CompilerException( |
1206
|
|
|
'Missing target selector for :extend()', |
1207
|
|
|
$index, |
1208
|
|
|
$this->context->currentFileInfo |
1209
|
|
|
); |
1210
|
|
|
} |
1211
|
|
|
|
1212
|
|
|
$extendList[] = new ExtendNode(new SelectorNode($elements), $option, $index); |
1213
|
|
|
} while ($this->input->char(',')); |
1214
|
|
|
|
1215
|
|
|
$this->expect('/\\G\)/'); |
1216
|
|
|
|
1217
|
|
|
if ($isRule) { |
1218
|
|
|
$this->expect('/\\G;/'); |
1219
|
|
|
} |
1220
|
|
|
|
1221
|
|
|
return $extendList; |
1222
|
|
|
} |
1223
|
|
|
|
1224
|
|
|
/** |
1225
|
|
|
* Parses extend rule. |
1226
|
|
|
* |
1227
|
|
|
* @return ExtendNode|null |
1228
|
|
|
*/ |
1229
|
|
|
protected function parseExtendRule() |
1230
|
|
|
{ |
1231
|
|
|
return $this->parseExtend(true); |
1232
|
|
|
} |
1233
|
|
|
|
1234
|
|
|
/** |
1235
|
|
|
* Parses a selector element. |
1236
|
|
|
* |
1237
|
|
|
* * `div` |
1238
|
|
|
* * `+ h1` |
1239
|
|
|
* * `#socks` |
1240
|
|
|
* * `input[type="text"]` |
1241
|
|
|
* |
1242
|
|
|
* Elements are the building blocks for selectors, |
1243
|
|
|
* they are made out of a `combinator` and an element name, such as a tag a class, or `*`. |
1244
|
|
|
* |
1245
|
|
|
* @return ElementNode|null |
1246
|
|
|
*/ |
1247
|
|
|
protected function parseElement() |
1248
|
|
|
{ |
1249
|
|
|
$index = $this->input->i; |
1250
|
|
|
|
1251
|
|
|
$c = $this->parseCombinator(); |
1252
|
|
|
|
1253
|
|
|
$e = $this->match( |
1254
|
|
|
[ |
1255
|
|
|
'/\\G^(?:\d+\.\d+|\d+)%/', |
1256
|
|
|
// http://stackoverflow.com/questions/3665962/regular-expression-error-no-ending-delimiter |
1257
|
|
|
'/\\G^(?:[.#]?|:*)(?:[\w-]|[^\\x{00}-\\x{9f}]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/', |
1258
|
|
|
'*', |
1259
|
|
|
'&', |
1260
|
|
|
'parseAttribute', |
1261
|
|
|
'/\\G^\([^&()@]+\)/', |
1262
|
|
|
'/\\G^[\.#:](?=@)/', |
1263
|
|
|
'parseEntitiesVariableCurly', |
1264
|
|
|
] |
1265
|
|
|
); |
1266
|
|
|
|
1267
|
|
|
if (!$e) { |
1268
|
|
|
$this->input->save(); |
1269
|
|
|
if ($this->input->char('(')) { |
1270
|
|
|
if (($v = $this->parseSelector()) && $this->input->char(')')) { |
1271
|
|
|
$e = new ParenNode($v); |
1272
|
|
|
$this->input->forget(); |
1273
|
|
|
} else { |
1274
|
|
|
$this->input->restore("Missing closing ')'"); |
1275
|
|
|
} |
1276
|
|
|
} else { |
1277
|
|
|
$this->input->forget(); |
1278
|
|
|
} |
1279
|
|
|
} |
1280
|
|
|
|
1281
|
|
|
if ($e) { |
1282
|
|
|
return new ElementNode($c, $e, $index, $this->context->currentFileInfo); |
|
|
|
|
1283
|
|
|
} |
1284
|
|
|
} |
1285
|
|
|
|
1286
|
|
|
/** |
1287
|
|
|
* Parses a combinator. Combinators combine elements together, in a selector. |
1288
|
|
|
* |
1289
|
|
|
* Because our parser isn't white-space sensitive, special care |
1290
|
|
|
* has to be taken, when parsing the descendant combinator, ` `, |
1291
|
|
|
* as it's an empty space. We have to check the previous character |
1292
|
|
|
* in the input, to see if it's a ` ` character. |
1293
|
|
|
* |
1294
|
|
|
* @return CombinatorNode|null |
1295
|
|
|
*/ |
1296
|
|
|
protected function parseCombinator() |
1297
|
|
|
{ |
1298
|
|
|
$c = $this->input->currentChar(); |
1299
|
|
|
|
1300
|
|
|
if ($c === '/') { |
1301
|
|
|
$this->input->save(); |
1302
|
|
|
$slashedCombinator = $this->input->re('/\\G^\/[a-z]+\//i'); |
1303
|
|
|
if ($slashedCombinator) { |
1304
|
|
|
$this->input->forget(); |
1305
|
|
|
|
1306
|
|
|
return new CombinatorNode($slashedCombinator); |
|
|
|
|
1307
|
|
|
} |
1308
|
|
|
$this->input->restore(); |
1309
|
|
|
} |
1310
|
|
|
|
1311
|
|
|
if ($c === '>' || $c === '+' || $c === '~' || $c === '|' || $c === '^') { |
1312
|
|
|
++$this->input->i; |
1313
|
|
|
if ($c === '^' && $this->input->currentChar() === '^') { |
1314
|
|
|
$c = '^^'; |
1315
|
|
|
++$this->input->i; |
1316
|
|
|
} |
1317
|
|
|
while ($this->input->isWhitespace()) { |
1318
|
|
|
++$this->input->i; |
1319
|
|
|
} |
1320
|
|
|
|
1321
|
|
|
return new CombinatorNode($c); |
1322
|
|
|
} elseif ($this->input->isWhiteSpace(-1)) { |
1323
|
|
|
return new CombinatorNode(' '); |
1324
|
|
|
} else { |
1325
|
|
|
return new CombinatorNode(); |
1326
|
|
|
} |
1327
|
|
|
} |
1328
|
|
|
|
1329
|
|
|
/** |
1330
|
|
|
* Parses an attribute. |
1331
|
|
|
* |
1332
|
|
|
* @return AttributeNode|null |
1333
|
|
|
*/ |
1334
|
|
|
protected function parseAttribute() |
1335
|
|
|
{ |
1336
|
|
|
if (!$this->input->char('[')) { |
1337
|
|
|
return; |
1338
|
|
|
} |
1339
|
|
|
|
1340
|
|
|
$key = $this->parseEntitiesVariableCurly(); |
1341
|
|
|
if (!$key) { |
1342
|
|
|
$key = $this->expect('/\\G(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\\\.)+/'); |
1343
|
|
|
} |
1344
|
|
|
|
1345
|
|
|
$val = null; |
1346
|
|
|
if (($op = $this->input->re('/\\G[|~*$^]?=/'))) { |
1347
|
|
|
$val = $this->match( |
1348
|
|
|
[ |
1349
|
|
|
'parseEntitiesQuoted', |
1350
|
|
|
'/\\G[0-9]+%/', |
1351
|
|
|
'/\\G[\w-]+/', |
1352
|
|
|
'parseEntitiesVariableCurly', |
1353
|
|
|
] |
1354
|
|
|
); |
1355
|
|
|
} |
1356
|
|
|
|
1357
|
|
|
$this->expect(']'); |
1358
|
|
|
|
1359
|
|
|
return new AttributeNode($key, $op, $val); |
|
|
|
|
1360
|
|
|
} |
1361
|
|
|
|
1362
|
|
|
/** |
1363
|
|
|
* Parses a value - a comma-delimited list of expressions like:. |
1364
|
|
|
* |
1365
|
|
|
* `font-family: Baskerville, Georgia, serif;` |
1366
|
|
|
* |
1367
|
|
|
* @return ValueNode|null |
1368
|
|
|
*/ |
1369
|
|
|
protected function parseValue() |
1370
|
|
|
{ |
1371
|
|
|
$e = null; |
1372
|
|
|
$expressions = []; |
1373
|
|
|
do { |
1374
|
|
|
$e = $this->parseExpression(); |
1375
|
|
|
if ($e) { |
1376
|
|
|
$expressions[] = $e; |
1377
|
|
|
if (!$this->input->char(',')) { |
1378
|
|
|
break; |
1379
|
|
|
} |
1380
|
|
|
} |
1381
|
|
|
} while ($e); |
1382
|
|
|
|
1383
|
|
|
if (count($expressions) > 0) { |
1384
|
|
|
return new ValueNode($expressions); |
1385
|
|
|
} |
1386
|
|
|
} |
1387
|
|
|
|
1388
|
|
|
/** |
1389
|
|
|
* Parses the `!important` keyword. |
1390
|
|
|
* |
1391
|
|
|
* @return string|null |
1392
|
|
|
*/ |
1393
|
|
|
protected function parseImportant() |
1394
|
|
|
{ |
1395
|
|
|
if ($this->input->currentChar() === '!') { |
1396
|
|
|
return $this->input->re('/\\G! *important/'); |
1397
|
|
|
} |
1398
|
|
|
} |
1399
|
|
|
|
1400
|
|
|
/** |
1401
|
|
|
* Parses a variable. |
1402
|
|
|
* |
1403
|
|
|
* @return string |
1404
|
|
|
*/ |
1405
|
|
|
protected function parseVariable() |
1406
|
|
|
{ |
1407
|
|
|
if ($this->input->currentChar() == '@' && ($name = $this->input->re('/\\G(@[\w-]+)\s*:/'))) { |
1408
|
|
|
return $name[1]; |
1409
|
|
|
} |
1410
|
|
|
} |
1411
|
|
|
|
1412
|
|
|
/** |
1413
|
|
|
* Parses a variable entity using the protective `{}` like: `@{variable}`. |
1414
|
|
|
* |
1415
|
|
|
* @return VariableNode|null |
1416
|
|
|
*/ |
1417
|
|
|
protected function parseEntitiesVariableCurly() |
1418
|
|
|
{ |
1419
|
|
|
$index = $this->input->i; |
1420
|
|
|
if ($this->input->currentChar() === '@' && ($curly = $this->input->re('/\\G@\{([\w-]+)\}/'))) { |
1421
|
|
|
return new VariableNode('@' . $curly[1], $index, $this->context->currentFileInfo); |
1422
|
|
|
} |
1423
|
|
|
} |
1424
|
|
|
|
1425
|
|
|
/** |
1426
|
|
|
* Parses rule property. |
1427
|
|
|
* |
1428
|
|
|
* @return array |
1429
|
|
|
*/ |
1430
|
|
|
protected function parseRuleProperty() |
1431
|
|
|
{ |
1432
|
|
|
$this->input->save(); |
1433
|
|
|
$index = []; |
1434
|
|
|
$name = []; |
1435
|
|
|
|
1436
|
|
|
$simpleProperty = $this->input->re('/\\G([_a-zA-Z0-9-]+)\s*:/'); |
1437
|
|
|
if ($simpleProperty) { |
1438
|
|
|
$name = new KeywordNode($simpleProperty[1]); |
1439
|
|
|
$this->input->forget(); |
1440
|
|
|
|
1441
|
|
|
return [$name]; |
1442
|
|
|
} |
1443
|
|
|
|
1444
|
|
|
// In PHP 5.3 we cannot use $this in the closure |
1445
|
|
|
$input = $this->input; |
1446
|
|
|
$match = function ($re) use (&$index, &$name, $input) { |
1447
|
|
|
$i = $input->i; |
1448
|
|
|
$chunk = $input->re($re); |
1449
|
|
|
if ($chunk) { |
1450
|
|
|
$index[] = $i; |
1451
|
|
|
$name[] = $chunk[1]; |
1452
|
|
|
|
1453
|
|
|
return count($name); |
1454
|
|
|
} |
1455
|
|
|
}; |
1456
|
|
|
|
1457
|
|
|
$match('/\\G(\*?)/'); |
1458
|
|
|
|
1459
|
|
|
while (true) { |
1460
|
|
|
if (!$match('/\\G((?:[\w-]+)|(?:@\{[\w-]+\}))/')) { |
1461
|
|
|
break; |
1462
|
|
|
} |
1463
|
|
|
} |
1464
|
|
|
|
1465
|
|
|
if (count($name) > 1 && $match('/\\G((?:\+_|\+)?)\s*:/')) { |
1466
|
|
|
$this->input->forget(); |
1467
|
|
|
|
1468
|
|
|
// at last, we have the complete match now. move forward, |
1469
|
|
|
// convert name particles to tree objects and return: |
1470
|
|
|
if ($name[0] === '') { |
1471
|
|
|
array_shift($name); |
1472
|
|
|
array_shift($index); |
1473
|
|
|
} |
1474
|
|
|
|
1475
|
|
|
for ($k = 0; $k < count($name); ++$k) { |
|
|
|
|
1476
|
|
|
$s = $name[$k]; |
1477
|
|
|
// intentionally @, the name can be an empty string |
1478
|
|
|
$name[$k] = @$s[0] !== '@' ? |
1479
|
|
|
new KeywordNode($s) : |
1480
|
|
|
new VariableNode('@' . substr($s, 2, -1), $index[$k], $this->context->currentFileInfo); |
1481
|
|
|
} |
1482
|
|
|
|
1483
|
|
|
return $name; |
1484
|
|
|
} |
1485
|
|
|
|
1486
|
|
|
$this->input->restore(); |
1487
|
|
|
} |
1488
|
|
|
|
1489
|
|
|
/** |
1490
|
|
|
* Parses an addition operation. |
1491
|
|
|
* |
1492
|
|
|
* @return OperationNode|null |
1493
|
|
|
*/ |
1494
|
|
|
protected function parseAddition() |
1495
|
|
|
{ |
1496
|
|
|
$operation = false; |
1497
|
|
|
if ($m = $this->parseMultiplication()) { |
1498
|
|
|
$isSpaced = $this->input->isWhitespace(-1); |
1499
|
|
|
while (true) { |
1500
|
|
|
$op = ($op = $this->input->re('/\\G[-+]\s+/')) ? $op : (!$isSpaced ? ($this->match( |
1501
|
|
|
['+', '-'] |
1502
|
|
|
)) : false); |
1503
|
|
|
if (!$op) { |
1504
|
|
|
break; |
1505
|
|
|
} |
1506
|
|
|
|
1507
|
|
|
$a = $this->parseMultiplication(); |
1508
|
|
|
if (!$a) { |
1509
|
|
|
break; |
1510
|
|
|
} |
1511
|
|
|
|
1512
|
|
|
$m->parensInOp = true; |
1513
|
|
|
$a->parensInOp = true; |
1514
|
|
|
|
1515
|
|
|
$operation = new OperationNode($op, [$operation ? $operation : $m, $a], $isSpaced); |
1516
|
|
|
$isSpaced = $this->input->isWhitespace(-1); |
1517
|
|
|
} |
1518
|
|
|
|
1519
|
|
|
return $operation ? $operation : $m; |
1520
|
|
|
} |
1521
|
|
|
} |
1522
|
|
|
|
1523
|
|
|
/** |
1524
|
|
|
* Parses multiplication operation. |
1525
|
|
|
* |
1526
|
|
|
* @return OperationNode|null |
1527
|
|
|
*/ |
1528
|
|
|
protected function parseMultiplication() |
1529
|
|
|
{ |
1530
|
|
|
$operation = null; |
1531
|
|
|
|
1532
|
|
|
if ($m = $this->parseOperand()) { |
1533
|
|
|
$isSpaced = $this->input->isWhitespace(-1); |
1534
|
|
|
while (true) { |
1535
|
|
|
if ($this->input->peek('/\\G\/[*\/]/')) { |
1536
|
|
|
break; |
1537
|
|
|
} |
1538
|
|
|
|
1539
|
|
|
$this->input->save(); |
1540
|
|
|
|
1541
|
|
|
$op = $this->match(['/', '*']); |
1542
|
|
|
|
1543
|
|
|
if (!$op) { |
1544
|
|
|
$this->input->forget(); |
1545
|
|
|
break; |
1546
|
|
|
} |
1547
|
|
|
|
1548
|
|
|
$a = $this->parseOperand(); |
1549
|
|
|
|
1550
|
|
|
if (!$a) { |
1551
|
|
|
$this->input->restore(); |
1552
|
|
|
break; |
1553
|
|
|
} |
1554
|
|
|
|
1555
|
|
|
$this->input->forget(); |
1556
|
|
|
|
1557
|
|
|
$m->parensInOp = true; |
1558
|
|
|
$a->parensInOp = true; |
1559
|
|
|
|
1560
|
|
|
$operation = new OperationNode($op, [$operation ? $operation : $m, $a], $isSpaced); |
|
|
|
|
1561
|
|
|
$isSpaced = $this->input->isWhitespace(-1); |
1562
|
|
|
} |
1563
|
|
|
|
1564
|
|
|
return $operation ? $operation : $m; |
1565
|
|
|
} |
1566
|
|
|
} |
1567
|
|
|
|
1568
|
|
|
/** |
1569
|
|
|
* Parses the conditions. |
1570
|
|
|
* |
1571
|
|
|
* @return ConditionNode|null |
1572
|
|
|
*/ |
1573
|
|
|
protected function parseConditions() |
1574
|
|
|
{ |
1575
|
|
|
$index = $this->input->i; |
1576
|
|
|
$condition = null; |
1577
|
|
|
if ($a = $this->parseCondition()) { |
1578
|
|
|
while (true) { |
1579
|
|
|
if (!$this->input->peekReg('/\\G,\s*(not\s*)?\(/') || !$this->input->char(',')) { |
1580
|
|
|
break; |
1581
|
|
|
} |
1582
|
|
|
$b = $this->parseCondition(); |
1583
|
|
|
if (!$b) { |
1584
|
|
|
break; |
1585
|
|
|
} |
1586
|
|
|
|
1587
|
|
|
$condition = new ConditionNode('or', $condition ? $condition : $a, $b, $index); |
1588
|
|
|
} |
1589
|
|
|
|
1590
|
|
|
return $condition ? $condition : $a; |
1591
|
|
|
} |
1592
|
|
|
} |
1593
|
|
|
|
1594
|
|
|
/** |
1595
|
|
|
* Parses condition. |
1596
|
|
|
* |
1597
|
|
|
* @return ConditionNode|null |
1598
|
|
|
* |
1599
|
|
|
* @throws ParserException |
1600
|
|
|
*/ |
1601
|
|
|
protected function parseCondition() |
1602
|
|
|
{ |
1603
|
|
|
$index = $this->input->i; |
1604
|
|
|
$negate = false; |
1605
|
|
|
|
1606
|
|
|
if ($this->input->str('not')) { |
1607
|
|
|
$negate = true; |
1608
|
|
|
} |
1609
|
|
|
|
1610
|
|
|
$this->expect('('); |
1611
|
|
|
if ($a = ($this->matchFuncs(['parseAddition', 'parseEntitiesKeyword', 'parseEntitiesQuoted']))) { |
1612
|
|
|
$op = null; |
1613
|
|
|
if ($this->input->char('>')) { |
1614
|
|
|
if ($this->input->char('=')) { |
1615
|
|
|
$op = '>='; |
1616
|
|
|
} else { |
1617
|
|
|
$op = '>'; |
1618
|
|
|
} |
1619
|
|
|
} elseif ($this->input->char('<')) { |
1620
|
|
|
if ($this->input->char('=')) { |
1621
|
|
|
$op = '<='; |
1622
|
|
|
} else { |
1623
|
|
|
$op = '<'; |
1624
|
|
|
} |
1625
|
|
|
} elseif ($this->input->char('=')) { |
1626
|
|
|
if ($this->input->char('>')) { |
1627
|
|
|
$op = '=>'; |
1628
|
|
|
} elseif ($this->input->char('<')) { |
1629
|
|
|
$op = '=<'; |
1630
|
|
|
} else { |
1631
|
|
|
$op = '='; |
1632
|
|
|
} |
1633
|
|
|
} |
1634
|
|
|
|
1635
|
|
|
$c = null; |
1636
|
|
|
if ($op) { |
1637
|
|
|
$b = $this->matchFuncs(['parseAddition', 'parseEntitiesKeyword', 'parseEntitiesQuoted']); |
1638
|
|
|
if ($b) { |
1639
|
|
|
$c = new ConditionNode($op, $a, $b, $index, $negate); |
1640
|
|
|
} else { |
1641
|
|
|
throw new ParserException('Unexpected expression', $index, $this->context->currentFileInfo); |
1642
|
|
|
} |
1643
|
|
|
} else { |
1644
|
|
|
$c = new ConditionNode('=', $a, new KeywordNode('true'), $index, $negate); |
1645
|
|
|
} |
1646
|
|
|
|
1647
|
|
|
$this->expect(')'); |
1648
|
|
|
|
1649
|
|
|
return $this->input->str('and') ? new ConditionNode('and', $c, $this->parseCondition()) : $c; |
|
|
|
|
1650
|
|
|
} |
1651
|
|
|
} |
1652
|
|
|
|
1653
|
|
|
/** |
1654
|
|
|
* Parses a sub-expression. |
1655
|
|
|
* |
1656
|
|
|
* @return ExpressionNode|null |
1657
|
|
|
*/ |
1658
|
|
|
protected function parseSubExpression() |
1659
|
|
|
{ |
1660
|
|
|
$this->input->save(); |
1661
|
|
|
|
1662
|
|
|
if ($this->input->char('(')) { |
1663
|
|
|
$a = $this->parseAddition(); |
1664
|
|
|
if ($a && $this->input->char(')')) { |
1665
|
|
|
$this->input->forget(); |
1666
|
|
|
$e = new ExpressionNode([$a]); |
1667
|
|
|
$e->parens = true; |
1668
|
|
|
|
1669
|
|
|
return $e; |
1670
|
|
|
} |
1671
|
|
|
|
1672
|
|
|
$this->input->restore("Expected ')'"); |
1673
|
|
|
|
1674
|
|
|
return; |
1675
|
|
|
} |
1676
|
|
|
|
1677
|
|
|
$this->input->restore(); |
1678
|
|
|
} |
1679
|
|
|
|
1680
|
|
|
/** |
1681
|
|
|
* Parses an operand. An operand is anything that can be part of an operation, |
1682
|
|
|
* such as a color, or a variable. |
1683
|
|
|
* |
1684
|
|
|
* @return NegativeNode|null |
1685
|
|
|
*/ |
1686
|
|
|
protected function parseOperand() |
1687
|
|
|
{ |
1688
|
|
|
$negate = false; |
1689
|
|
|
if ($this->input->peekReg('/\\G^-[@\(]/')) { |
1690
|
|
|
$negate = $this->input->char('-'); |
1691
|
|
|
} |
1692
|
|
|
|
1693
|
|
|
$o = $this->matchFuncs( |
1694
|
|
|
[ |
1695
|
|
|
'parseSubExpression', |
1696
|
|
|
'parseEntitiesDimension', |
1697
|
|
|
'parseEntitiesColor', |
1698
|
|
|
'parseEntitiesVariable', |
1699
|
|
|
'parseEntitiesCall', |
1700
|
|
|
] |
1701
|
|
|
); |
1702
|
|
|
|
1703
|
|
|
if ($negate) { |
1704
|
|
|
$o->parensInOp = true; |
1705
|
|
|
$o = new NegativeNode($o); |
1706
|
|
|
} |
1707
|
|
|
|
1708
|
|
|
return $o; |
1709
|
|
|
} |
1710
|
|
|
|
1711
|
|
|
/** |
1712
|
|
|
* Parses a block. The `block` rule is used by `ruleset` and `mixin definition`. |
1713
|
|
|
* It's a wrapper around the `primary` rule, with added `{}`. |
1714
|
|
|
* |
1715
|
|
|
* @return array |
1716
|
|
|
*/ |
1717
|
|
|
protected function parseBlock() |
1718
|
|
|
{ |
1719
|
|
|
if ($this->input->char('{') && (is_array($content = $this->parsePrimary())) && $this->input->char('}')) { |
1720
|
|
|
return $content; |
1721
|
|
|
} |
1722
|
|
|
} |
1723
|
|
|
|
1724
|
|
|
/** |
1725
|
|
|
* @return Node|RulesetNode|null |
1726
|
|
|
*/ |
1727
|
|
|
protected function parseBlockRuleset() |
1728
|
|
|
{ |
1729
|
|
|
$block = $this->parseBlock(); |
1730
|
|
|
if (null !== $block) { |
1731
|
|
|
$block = new RulesetNode([], $block); |
1732
|
|
|
} |
1733
|
|
|
|
1734
|
|
|
return $block; |
1735
|
|
|
} |
1736
|
|
|
|
1737
|
|
|
/** |
1738
|
|
|
* @return DetachedRulesetNode|null |
1739
|
|
|
*/ |
1740
|
|
|
protected function parseDetachedRuleset() |
1741
|
|
|
{ |
1742
|
|
|
$blockRuleset = $this->parseBlockRuleset(); |
1743
|
|
|
if ($blockRuleset) { |
1744
|
|
|
return new DetachedRulesetNode($blockRuleset); |
1745
|
|
|
} |
1746
|
|
|
} |
1747
|
|
|
|
1748
|
|
|
/** |
1749
|
|
|
* Parses comments. |
1750
|
|
|
* |
1751
|
|
|
* @return array Array of comments |
1752
|
|
|
*/ |
1753
|
|
|
protected function parseComments() |
1754
|
|
|
{ |
1755
|
|
|
$comments = []; |
1756
|
|
|
while ($comment = $this->parseComment()) { |
1757
|
|
|
$comments[] = $comment; |
1758
|
|
|
} |
1759
|
|
|
|
1760
|
|
|
return $comments; |
1761
|
|
|
} |
1762
|
|
|
|
1763
|
|
|
/** |
1764
|
|
|
* Parses the CSS directive like:. |
1765
|
|
|
* |
1766
|
|
|
* <pre> |
1767
|
|
|
* |
1768
|
|
|
* @charset "utf-8"; |
1769
|
|
|
* </pre> |
1770
|
|
|
* |
1771
|
|
|
* @return DirectiveNode|null |
1772
|
|
|
* |
1773
|
|
|
* @throws ParserException |
1774
|
|
|
*/ |
1775
|
|
|
protected function parseDirective() |
1776
|
|
|
{ |
1777
|
|
|
$hasBlock = true; |
1778
|
|
|
$hasIdentifier = false; |
1779
|
|
|
$hasExpression = false; |
1780
|
|
|
$isRooted = true; |
1781
|
|
|
$rules = null; |
1782
|
|
|
$hasUnknown = null; |
1783
|
|
|
$index = $this->input->i; |
1784
|
|
|
|
1785
|
|
|
if ($this->input->currentChar() !== '@') { |
1786
|
|
|
return; |
1787
|
|
|
} |
1788
|
|
|
|
1789
|
|
|
$value = $this->matchFuncs(['parseImport', 'parsePlugin', 'parseMedia']); |
1790
|
|
|
|
1791
|
|
|
if ($value) { |
1792
|
|
|
return $value; |
1793
|
|
|
} |
1794
|
|
|
|
1795
|
|
|
$this->input->save(); |
1796
|
|
|
|
1797
|
|
|
$name = $this->input->re('/\\G@[a-z-]+/'); |
1798
|
|
|
|
1799
|
|
|
if (!$name) { |
1800
|
|
|
return; |
1801
|
|
|
} |
1802
|
|
|
|
1803
|
|
|
$nonVendorSpecificName = $name; |
1804
|
|
|
$pos = strpos($name, '-', 2); |
1805
|
|
|
if ($name[1] == '-' && $pos > 0) { |
1806
|
|
|
$nonVendorSpecificName = '@' . substr($name, $pos + 1); |
1807
|
|
|
} |
1808
|
|
|
|
1809
|
|
|
switch ($nonVendorSpecificName) { |
1810
|
|
|
/* |
1811
|
|
|
case '@font-face': |
1812
|
|
|
case '@viewport': |
1813
|
|
|
case '@top-left': |
1814
|
|
|
case '@top-left-corner': |
1815
|
|
|
case '@top-center': |
1816
|
|
|
case '@top-right': |
1817
|
|
|
case '@top-right-corner': |
1818
|
|
|
case '@bottom-left': |
1819
|
|
|
case '@bottom-left-corner': |
1820
|
|
|
case '@bottom-center': |
1821
|
|
|
case '@bottom-right': |
1822
|
|
|
case '@bottom-right-corner': |
1823
|
|
|
case '@left-top': |
1824
|
|
|
case '@left-middle': |
1825
|
|
|
case '@left-bottom': |
1826
|
|
|
case '@right-top': |
1827
|
|
|
case '@right-middle': |
1828
|
|
|
case '@right-bottom': |
1829
|
|
|
$hasBlock = true; |
1830
|
|
|
$isRooted = true; |
1831
|
|
|
break; |
1832
|
|
|
*/ |
1833
|
|
|
case '@counter-style': |
1834
|
|
|
$hasIdentifier = true; |
1835
|
|
|
$hasBlock = true; |
1836
|
|
|
break; |
1837
|
|
|
case '@charset': |
1838
|
|
|
$hasIdentifier = true; |
1839
|
|
|
$hasBlock = false; |
1840
|
|
|
break; |
1841
|
|
|
case '@namespace': |
1842
|
|
|
$hasExpression = true; |
1843
|
|
|
$hasBlock = false; |
1844
|
|
|
break; |
1845
|
|
|
case '@keyframes': |
1846
|
|
|
$hasIdentifier = true; |
1847
|
|
|
break; |
1848
|
|
|
case '@host': |
1849
|
|
|
case '@page': |
1850
|
|
|
$hasUnknown = true; |
1851
|
|
|
break; |
1852
|
|
|
case '@document': |
1853
|
|
|
case '@supports': |
1854
|
|
|
$hasUnknown = true; |
1855
|
|
|
$isRooted = false; |
1856
|
|
|
break; |
1857
|
|
|
|
1858
|
|
|
} |
1859
|
|
|
|
1860
|
|
|
$this->input->commentStore = []; |
1861
|
|
|
|
1862
|
|
|
if ($hasIdentifier) { |
1863
|
|
|
$value = $this->parseEntity(); |
1864
|
|
|
if (!$value) { |
1865
|
|
|
throw new ParserException(sprintf('Expected %s identifier', $name)); |
1866
|
|
|
} |
1867
|
|
|
} elseif ($hasExpression) { |
1868
|
|
|
$value = $this->parseExpression(); |
1869
|
|
|
if (!$value) { |
1870
|
|
|
throw new ParserException(sprintf('Expected %s expression', $name)); |
1871
|
|
|
} |
1872
|
|
|
} elseif ($hasUnknown) { |
1873
|
|
|
$value = $this->input->re('/\\G^[^{;]+/'); |
1874
|
|
|
$value = trim((string) $value); |
1875
|
|
|
if ($value) { |
1876
|
|
|
$value = new AnonymousNode($value); |
1877
|
|
|
} |
1878
|
|
|
} |
1879
|
|
|
|
1880
|
|
|
if ($hasBlock) { |
1881
|
|
|
$rules = $this->parseBlockRuleset(); |
1882
|
|
|
} |
1883
|
|
|
|
1884
|
|
|
if ($rules || (!$hasBlock && $value && $this->input->char(';'))) { |
1885
|
|
|
$this->input->forget(); |
1886
|
|
|
|
1887
|
|
|
return new DirectiveNode( |
1888
|
|
|
$name, $value, $rules, $index, $this->context->currentFileInfo, |
|
|
|
|
1889
|
|
|
$this->context->dumpLineNumbers ? $this->getDebugInfo($index) : null, |
1890
|
|
|
false, $isRooted |
1891
|
|
|
); |
1892
|
|
|
} |
1893
|
|
|
|
1894
|
|
|
$this->input->restore('Directive options not recognised'); |
1895
|
|
|
} |
1896
|
|
|
|
1897
|
|
|
/** |
1898
|
|
|
* Entities are the smallest recognized token, and can be found inside a rule's value. |
1899
|
|
|
* |
1900
|
|
|
* @return Node|null |
1901
|
|
|
*/ |
1902
|
|
|
protected function parseEntity() |
1903
|
|
|
{ |
1904
|
|
|
return $this->matchFuncs( |
1905
|
|
|
[ |
1906
|
|
|
'parseComment', |
1907
|
|
|
'parseEntitiesLiteral', |
1908
|
|
|
'parseEntitiesVariable', |
1909
|
|
|
'parseEntitiesUrl', |
1910
|
|
|
'parseEntitiesCall', |
1911
|
|
|
'parseEntitiesKeyword', |
1912
|
|
|
'parseEntitiesJavascript', |
1913
|
|
|
] |
1914
|
|
|
); |
1915
|
|
|
} |
1916
|
|
|
|
1917
|
|
|
/** |
1918
|
|
|
* Parse entities literal. |
1919
|
|
|
* |
1920
|
|
|
* @return Node|null |
1921
|
|
|
*/ |
1922
|
|
|
protected function parseEntitiesLiteral() |
1923
|
|
|
{ |
1924
|
|
|
return $this->matchFuncs( |
1925
|
|
|
[ |
1926
|
|
|
'parseEntitiesDimension', |
1927
|
|
|
'parseEntitiesColor', |
1928
|
|
|
'parseEntitiesQuoted', |
1929
|
|
|
'parseUnicodeDescriptor', |
1930
|
|
|
] |
1931
|
|
|
); |
1932
|
|
|
} |
1933
|
|
|
|
1934
|
|
|
/** |
1935
|
|
|
* Parses an entity variable. |
1936
|
|
|
* |
1937
|
|
|
* @return VariableNode|null |
1938
|
|
|
*/ |
1939
|
|
|
protected function parseEntitiesVariable() |
1940
|
|
|
{ |
1941
|
|
|
$index = $this->input->i; |
1942
|
|
|
if ($this->input->currentChar() === '@' && ($name = $this->input->re('/\\G^@@?[\w-]+/'))) { |
1943
|
|
|
return new VariableNode($name, $index, $this->context->currentFileInfo); |
|
|
|
|
1944
|
|
|
} |
1945
|
|
|
} |
1946
|
|
|
|
1947
|
|
|
/** |
1948
|
|
|
* Parse entities dimension (a number and a unit like 0.5em, 95%). |
1949
|
|
|
* |
1950
|
|
|
* @return DimensionNode|null |
1951
|
|
|
*/ |
1952
|
|
|
protected function parseEntitiesDimension() |
1953
|
|
|
{ |
1954
|
|
|
if ($this->input->peekNotNumeric()) { |
1955
|
|
|
return; |
1956
|
|
|
} |
1957
|
|
|
|
1958
|
|
|
if ($value = $this->input->re('/\\G^([+-]?\d*\.?\d+)(%|[a-z]+)?/i')) { |
1959
|
|
|
return new DimensionNode($value[1], isset($value[2]) ? $value[2] : null); |
1960
|
|
|
} |
1961
|
|
|
} |
1962
|
|
|
|
1963
|
|
|
/** |
1964
|
|
|
* Parses a hexadecimal color. |
1965
|
|
|
* |
1966
|
|
|
* @return ColorNode |
1967
|
|
|
* |
1968
|
|
|
* @throws ParserException |
1969
|
|
|
*/ |
1970
|
|
|
protected function parseEntitiesColor() |
1971
|
|
|
{ |
1972
|
|
|
// we are more tolerate here than in less.js, which can use regexp input property |
1973
|
|
|
// to get the regular expression input, the regexp includes A-z but the color hex code is only A-F |
1974
|
|
|
if ($this->input->currentChar() === '#' && ($rgb = $this->input->re('/\\G#([A-Za-z0-9]{6}|[A-Za-z0-9]{3})/'))) { |
1975
|
|
|
$colorCandidate = $rgb[1]; |
1976
|
|
|
// verify if candidate consists only of allowed HEX characters |
1977
|
|
|
if (!preg_match('/^[A-Fa-f0-9]+$/', $colorCandidate)) { |
1978
|
|
|
throw new ParserException('Invalid HEX color code', $this->input->i, |
1979
|
|
|
$this->context->currentFileInfo); |
1980
|
|
|
} |
1981
|
|
|
|
1982
|
|
|
return new ColorNode($colorCandidate, null, '#' . $colorCandidate); |
1983
|
|
|
} |
1984
|
|
|
} |
1985
|
|
|
|
1986
|
|
|
/** |
1987
|
|
|
* Parses a string, which supports escaping " and ' |
1988
|
|
|
* "milky way" 'he\'s the one!'. |
1989
|
|
|
* |
1990
|
|
|
* @return QuotedNode|null |
1991
|
|
|
*/ |
1992
|
|
|
protected function parseEntitiesQuoted() |
1993
|
|
|
{ |
1994
|
|
|
$isEscaped = false; |
1995
|
|
|
$index = $this->input->i; |
1996
|
|
|
|
1997
|
|
|
$this->input->save(); |
1998
|
|
|
|
1999
|
|
|
if ($this->input->char('~')) { |
2000
|
|
|
$isEscaped = true; |
2001
|
|
|
} |
2002
|
|
|
|
2003
|
|
|
$str = $this->input->quoted(); |
2004
|
|
|
|
2005
|
|
|
if (!$str) { |
2006
|
|
|
$this->input->restore(); |
2007
|
|
|
|
2008
|
|
|
return; |
2009
|
|
|
} |
2010
|
|
|
|
2011
|
|
|
$this->input->forget(); |
2012
|
|
|
|
2013
|
|
|
return new QuotedNode( |
2014
|
|
|
$str[0], |
2015
|
|
|
substr($str, 1, strlen($str) - 2), |
2016
|
|
|
$isEscaped, |
2017
|
|
|
$index, |
2018
|
|
|
$this->context->currentFileInfo |
2019
|
|
|
); |
2020
|
|
|
} |
2021
|
|
|
|
2022
|
|
|
/** |
2023
|
|
|
* Parses an unicode descriptor, as is used in unicode-range U+0?? or U+00A1-00A9. |
2024
|
|
|
* |
2025
|
|
|
* @return UnicodeDescriptorNode|null |
2026
|
|
|
*/ |
2027
|
|
|
protected function parseUnicodeDescriptor() |
2028
|
|
|
{ |
2029
|
|
|
if ($ud = $this->input->re('/\\G(U\+[0-9a-fA-F?]+)(\-[0-9a-fA-F?]+)?/')) { |
2030
|
|
|
return new UnicodeDescriptorNode($ud[0]); |
2031
|
|
|
} |
2032
|
|
|
} |
2033
|
|
|
|
2034
|
|
|
/** |
2035
|
|
|
* A catch-all word, such as: `black border-collapse`. |
2036
|
|
|
* |
2037
|
|
|
* @return ColorNode|KeywordNode|null |
2038
|
|
|
*/ |
2039
|
|
|
protected function parseEntitiesKeyword() |
2040
|
|
|
{ |
2041
|
|
|
$k = $this->input->char('%'); |
2042
|
|
|
if (!$k) { |
2043
|
|
|
$k = $this->input->re('/\\G[_A-Za-z-][_A-Za-z0-9-]*/'); |
2044
|
|
|
} |
2045
|
|
|
|
2046
|
|
|
if ($k) { |
2047
|
|
|
// detected named color and "transparent" keyword |
2048
|
|
|
if ($color = Color::fromKeyword($k)) { |
2049
|
|
|
return new ColorNode($color); |
2050
|
|
|
} else { |
2051
|
|
|
return new KeywordNode($k); |
2052
|
|
|
} |
2053
|
|
|
} |
2054
|
|
|
} |
2055
|
|
|
|
2056
|
|
|
/** |
2057
|
|
|
* Parses url() tokens. |
2058
|
|
|
* |
2059
|
|
|
* @return UrlNode|null |
2060
|
|
|
*/ |
2061
|
|
|
protected function parseEntitiesUrl() |
2062
|
|
|
{ |
2063
|
|
|
$index = $this->input->i; |
2064
|
|
|
$this->input->autoCommentAbsorb = false; |
2065
|
|
|
|
2066
|
|
|
if (!$this->input->str('url(')) { |
2067
|
|
|
$this->input->autoCommentAbsorb = true; |
2068
|
|
|
|
2069
|
|
|
return; |
2070
|
|
|
} |
2071
|
|
|
|
2072
|
|
|
$value = $this->match( |
2073
|
|
|
[ |
2074
|
|
|
'parseEntitiesQuoted', |
2075
|
|
|
'parseEntitiesVariable', |
2076
|
|
|
'/\\G(?>[^\\(\\)\'"]+|(?<=\\\\)[\\(\\)\'"])+/', |
2077
|
|
|
] |
2078
|
|
|
); |
2079
|
|
|
|
2080
|
|
|
$this->input->autoCommentAbsorb = true; |
2081
|
|
|
|
2082
|
|
|
$this->expect(')'); |
2083
|
|
|
|
2084
|
|
|
return new UrlNode( |
2085
|
|
|
(isset($value->value) || $value instanceof VariableNode) ? $value : new AnonymousNode( |
|
|
|
|
2086
|
|
|
(string) $value |
2087
|
|
|
), |
2088
|
|
|
$index, |
2089
|
|
|
$this->context->currentFileInfo |
2090
|
|
|
); |
2091
|
|
|
} |
2092
|
|
|
|
2093
|
|
|
/** |
2094
|
|
|
* Parses a function call. |
2095
|
|
|
* |
2096
|
|
|
* @return CallNode|null |
2097
|
|
|
*/ |
2098
|
|
|
protected function parseEntitiesCall() |
2099
|
|
|
{ |
2100
|
|
|
if ($this->input->peekReg('/\\G^url\(/i')) { |
2101
|
|
|
return; |
2102
|
|
|
} |
2103
|
|
|
|
2104
|
|
|
$index = $this->input->i; |
2105
|
|
|
|
2106
|
|
|
$this->input->save(); |
2107
|
|
|
|
2108
|
|
|
$name = $this->input->re('/\\G([\w-]+|%|progid:[\w\.]+)\(/'); |
2109
|
|
|
|
2110
|
|
|
if (!$name) { |
2111
|
|
|
$this->input->forget(); |
2112
|
|
|
|
2113
|
|
|
return; |
2114
|
|
|
} |
2115
|
|
|
|
2116
|
|
|
$name = $name[1]; |
2117
|
|
|
$nameLC = strtolower($name); |
2118
|
|
|
|
2119
|
|
|
if ($nameLC === 'alpha') { |
2120
|
|
|
$alpha = $this->parseAlpha(); |
2121
|
|
|
if ($alpha) { |
2122
|
|
|
$this->input->forget(); |
2123
|
|
|
|
2124
|
|
|
return $alpha; |
2125
|
|
|
} |
2126
|
|
|
} |
2127
|
|
|
|
2128
|
|
|
$args = $this->parseEntitiesArguments(); |
2129
|
|
|
|
2130
|
|
|
if (!$this->input->char(')')) { |
2131
|
|
|
$this->input->restore("Could not parse call arguments or missing ')'"); |
2132
|
|
|
|
2133
|
|
|
return; |
2134
|
|
|
} |
2135
|
|
|
|
2136
|
|
|
$this->input->forget(); |
2137
|
|
|
|
2138
|
|
|
return new CallNode($name, $args, $index, $this->context->currentFileInfo); |
2139
|
|
|
} |
2140
|
|
|
|
2141
|
|
|
/** |
2142
|
|
|
* Parse a list of arguments. |
2143
|
|
|
* |
2144
|
|
|
* @return array |
2145
|
|
|
*/ |
2146
|
|
|
protected function parseEntitiesArguments() |
2147
|
|
|
{ |
2148
|
|
|
$args = []; |
2149
|
|
|
|
2150
|
|
|
while (true) { |
2151
|
|
|
$arg = $this->matchFuncs(['parseEntitiesAssignment', 'parseExpression']); |
2152
|
|
|
if (!$arg) { |
2153
|
|
|
break; |
2154
|
|
|
} |
2155
|
|
|
$args[] = $arg; |
2156
|
|
|
if (!$this->input->char(',')) { |
2157
|
|
|
break; |
2158
|
|
|
} |
2159
|
|
|
} |
2160
|
|
|
|
2161
|
|
|
return $args; |
2162
|
|
|
} |
2163
|
|
|
|
2164
|
|
|
/** |
2165
|
|
|
* Parses an assignments (argument entities for calls). |
2166
|
|
|
* They are present in ie filter properties as shown below. |
2167
|
|
|
* filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* ). |
2168
|
|
|
* |
2169
|
|
|
* @return AssignmentNode|null |
2170
|
|
|
*/ |
2171
|
|
|
protected function parseEntitiesAssignment() |
2172
|
|
|
{ |
2173
|
|
|
$this->input->save(); |
2174
|
|
|
$key = $this->input->re('/\\G\w+(?=\s?=)/i'); |
2175
|
|
|
if (!$key) { |
2176
|
|
|
$this->input->restore(); |
2177
|
|
|
|
2178
|
|
|
return; |
2179
|
|
|
} |
2180
|
|
|
|
2181
|
|
|
if (!$this->input->char('=')) { |
2182
|
|
|
$this->input->restore(); |
2183
|
|
|
|
2184
|
|
|
return; |
2185
|
|
|
} |
2186
|
|
|
|
2187
|
|
|
$value = $this->parseEntity(); |
2188
|
|
|
if ($value) { |
2189
|
|
|
$this->input->forget(); |
2190
|
|
|
|
2191
|
|
|
return new AssignmentNode($key, $value); |
|
|
|
|
2192
|
|
|
} else { |
2193
|
|
|
$this->input->restore(); |
2194
|
|
|
} |
2195
|
|
|
} |
2196
|
|
|
|
2197
|
|
|
/** |
2198
|
|
|
* Parses an expression. Expressions either represent mathematical operations, |
2199
|
|
|
* or white-space delimited entities like: `1px solid black`, `@var * 2`. |
2200
|
|
|
* |
2201
|
|
|
* @return ExpressionNode|null |
2202
|
|
|
*/ |
2203
|
|
|
protected function parseExpression() |
2204
|
|
|
{ |
2205
|
|
|
$entities = []; |
2206
|
|
|
$e = null; |
2207
|
|
|
do { |
2208
|
|
|
$e = $this->parseComment(); |
2209
|
|
|
if ($e) { |
2210
|
|
|
$entities[] = $e; |
2211
|
|
|
continue; |
2212
|
|
|
} |
2213
|
|
|
|
2214
|
|
|
$e = $this->matchFuncs(['parseAddition', 'parseEntity']); |
2215
|
|
|
if ($e) { |
2216
|
|
|
$entities[] = $e; |
2217
|
|
|
// operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here |
2218
|
|
|
if (!$this->input->peekReg('/\\G\/[\/*]/')) { |
2219
|
|
|
$delim = $this->input->char('/'); |
2220
|
|
|
if ($delim) { |
2221
|
|
|
$entities[] = new AnonymousNode($delim); |
2222
|
|
|
} |
2223
|
|
|
} |
2224
|
|
|
} |
2225
|
|
|
} while ($e); |
2226
|
|
|
|
2227
|
|
|
if (count($entities) > 0) { |
2228
|
|
|
return new ExpressionNode($entities); |
2229
|
|
|
} |
2230
|
|
|
} |
2231
|
|
|
|
2232
|
|
|
/** |
2233
|
|
|
* Parses IE's alpha function `alpha(opacity=88)`. |
2234
|
|
|
* |
2235
|
|
|
* @return AlphaNode|null |
2236
|
|
|
*/ |
2237
|
|
|
protected function parseAlpha() |
2238
|
|
|
{ |
2239
|
|
|
if (!$this->input->re('/\\G^opacity=/i')) { |
2240
|
|
|
return; |
2241
|
|
|
} |
2242
|
|
|
|
2243
|
|
|
$value = $this->input->re('/\\G^\d+/'); |
2244
|
|
|
if ($value === null) { |
2245
|
|
|
$value = $this->parseEntitiesVariable(); |
2246
|
|
|
if (!$value) { |
2247
|
|
|
throw new ParserException('Could not parse alpha', $this->input->i, $this->context->currentFileInfo); |
2248
|
|
|
} |
2249
|
|
|
} |
2250
|
|
|
|
2251
|
|
|
$this->expect(')'); |
2252
|
|
|
|
2253
|
|
|
return new AlphaNode($value); |
2254
|
|
|
} |
2255
|
|
|
|
2256
|
|
|
/** |
2257
|
|
|
* Parses a javascript code. |
2258
|
|
|
* |
2259
|
|
|
* @return JavascriptNode|null |
2260
|
|
|
*/ |
2261
|
|
|
protected function parseEntitiesJavascript() |
2262
|
|
|
{ |
2263
|
|
|
$index = $this->input->i; |
2264
|
|
|
|
2265
|
|
|
$this->input->save(); |
2266
|
|
|
|
2267
|
|
|
$escape = $this->input->char('~'); |
2268
|
|
|
$jsQuote = $this->input->char('`'); |
2269
|
|
|
|
2270
|
|
|
if (!$jsQuote) { |
2271
|
|
|
$this->input->restore(); |
2272
|
|
|
|
2273
|
|
|
return; |
2274
|
|
|
} |
2275
|
|
|
|
2276
|
|
|
if ($js = $this->input->re('/\\G^[^`]*`/')) { |
2277
|
|
|
$this->input->forget(); |
2278
|
|
|
|
2279
|
|
|
return new JavascriptNode( |
2280
|
|
|
substr($js, 0, strlen($js) - 1), |
2281
|
|
|
(bool) $escape, |
2282
|
|
|
$index, |
2283
|
|
|
$this->context->currentFileInfo |
2284
|
|
|
); |
2285
|
|
|
} else { |
2286
|
|
|
$this->input->restore('Invalid javascript definition'); |
2287
|
|
|
} |
2288
|
|
|
} |
2289
|
|
|
|
2290
|
|
|
/** |
2291
|
|
|
* Parses a @import directive. |
2292
|
|
|
* |
2293
|
|
|
* @return ImportNode|null |
2294
|
|
|
* |
2295
|
|
|
* @throws ParserException |
2296
|
|
|
*/ |
2297
|
|
|
protected function parseImport() |
2298
|
|
|
{ |
2299
|
|
|
$index = $this->input->i; |
2300
|
|
|
$dir = $this->input->re('/\\G^@import?\s+/'); |
2301
|
|
|
|
2302
|
|
|
if ($dir) { |
2303
|
|
|
$options = $this->parseImportOptions(); |
2304
|
|
|
if (!$options) { |
2305
|
|
|
$options = []; |
2306
|
|
|
} |
2307
|
|
|
|
2308
|
|
|
if (($path = $this->matchFuncs(['parseEntitiesQuoted', 'parseEntitiesUrl']))) { |
2309
|
|
|
$features = $this->parseMediaFeatures(); |
2310
|
|
|
|
2311
|
|
|
if (!$this->input->char(';')) { |
2312
|
|
|
$this->input->i = $index; |
2313
|
|
|
throw new ParserException( |
2314
|
|
|
'Missing semi-colon or unrecognised media features on import', |
2315
|
|
|
$index, |
2316
|
|
|
$this->context->currentFileInfo |
2317
|
|
|
); |
2318
|
|
|
} |
2319
|
|
|
|
2320
|
|
|
if ($features) { |
2321
|
|
|
$features = new ValueNode($features); |
2322
|
|
|
} |
2323
|
|
|
|
2324
|
|
|
return new ImportNode($path, $features, $options, $index, $this->context->currentFileInfo); |
|
|
|
|
2325
|
|
|
} else { |
2326
|
|
|
$this->input->i = $index; |
2327
|
|
|
throw new ParserException('Malformed import statement', $index, $this->context->currentFileInfo); |
2328
|
|
|
} |
2329
|
|
|
} |
2330
|
|
|
} |
2331
|
|
|
|
2332
|
|
|
/** |
2333
|
|
|
* Parses import options. |
2334
|
|
|
* |
2335
|
|
|
* @return array |
2336
|
|
|
*/ |
2337
|
|
|
protected function parseImportOptions() |
2338
|
|
|
{ |
2339
|
|
|
// list of options, surrounded by parens |
2340
|
|
|
if (!$this->input->char('(')) { |
2341
|
|
|
return; |
2342
|
|
|
} |
2343
|
|
|
|
2344
|
|
|
$options = []; |
2345
|
|
|
|
2346
|
|
|
do { |
2347
|
|
|
if ($o = $this->parseImportOption()) { |
2348
|
|
|
$optionName = $o; |
2349
|
|
|
$value = true; |
2350
|
|
|
switch ($optionName) { |
2351
|
|
|
case 'css': |
2352
|
|
|
$optionName = 'less'; |
2353
|
|
|
$value = false; |
2354
|
|
|
break; |
2355
|
|
|
case 'once': |
2356
|
|
|
$optionName = 'multiple'; |
2357
|
|
|
$value = false; |
2358
|
|
|
break; |
2359
|
|
|
} |
2360
|
|
|
$options[$optionName] = $value; |
2361
|
|
|
if (!$this->input->char(',')) { |
2362
|
|
|
break; |
2363
|
|
|
} |
2364
|
|
|
} |
2365
|
|
|
} while ($o); |
2366
|
|
|
|
2367
|
|
|
$this->expect(')'); |
2368
|
|
|
|
2369
|
|
|
return $options; |
2370
|
|
|
} |
2371
|
|
|
|
2372
|
|
|
/** |
2373
|
|
|
* Parses import option. |
2374
|
|
|
* |
2375
|
|
|
* @return string|null |
2376
|
|
|
*/ |
2377
|
|
|
protected function parseImportOption() |
2378
|
|
|
{ |
2379
|
|
|
if (($opt = $this->input->re('/\\G(less|css|multiple|once|inline|reference|optional)/'))) { |
2380
|
|
|
return $opt[1]; |
2381
|
|
|
} |
2382
|
|
|
} |
2383
|
|
|
|
2384
|
|
|
/** |
2385
|
|
|
* Parses media block. |
2386
|
|
|
* |
2387
|
|
|
* @return MediaNode|null |
2388
|
|
|
*/ |
2389
|
|
|
protected function parseMedia() |
2390
|
|
|
{ |
2391
|
|
|
$debugInfo = null; |
2392
|
|
|
if ($this->context->dumpLineNumbers) { |
2393
|
|
|
$debugInfo = $this->getDebugInfo($this->input->i); |
2394
|
|
|
} |
2395
|
|
|
|
2396
|
|
|
$this->input->save(); |
2397
|
|
|
|
2398
|
|
|
if ($this->input->str('@media')) { |
2399
|
|
|
$features = $this->parseMediaFeatures(); |
2400
|
|
|
$rules = $this->parseBlock(); |
2401
|
|
|
|
2402
|
|
|
if (null === $rules) { |
2403
|
|
|
$this->input->restore('Media definitions require block statements after any features'); |
2404
|
|
|
|
2405
|
|
|
return; |
2406
|
|
|
} |
2407
|
|
|
|
2408
|
|
|
$this->input->forget(); |
2409
|
|
|
|
2410
|
|
|
$media = new MediaNode($rules, $features, $this->input->i, $this->context->currentFileInfo); |
|
|
|
|
2411
|
|
|
|
2412
|
|
|
if ($debugInfo) { |
2413
|
|
|
$media->debugInfo = $debugInfo; |
2414
|
|
|
} |
2415
|
|
|
|
2416
|
|
|
return $media; |
2417
|
|
|
} |
2418
|
|
|
|
2419
|
|
|
$this->input->restore(); |
2420
|
|
|
} |
2421
|
|
|
|
2422
|
|
|
/** |
2423
|
|
|
* Parses media features. |
2424
|
|
|
* |
2425
|
|
|
* @return array |
2426
|
|
|
*/ |
2427
|
|
|
protected function parseMediaFeatures() |
2428
|
|
|
{ |
2429
|
|
|
$features = []; |
2430
|
|
|
do { |
2431
|
|
|
if ($e = $this->parseMediaFeature()) { |
2432
|
|
|
$features[] = $e; |
2433
|
|
|
if (!$this->input->char(',')) { |
2434
|
|
|
break; |
2435
|
|
|
} |
2436
|
|
|
} elseif ($e = $this->parseEntitiesVariable()) { |
2437
|
|
|
$features[] = $e; |
2438
|
|
|
if (!$this->input->char(',')) { |
2439
|
|
|
break; |
2440
|
|
|
} |
2441
|
|
|
} |
2442
|
|
|
} while ($e); |
2443
|
|
|
|
2444
|
|
|
return $features ? $features : null; |
2445
|
|
|
} |
2446
|
|
|
|
2447
|
|
|
/** |
2448
|
|
|
* Parses single media feature. |
2449
|
|
|
* |
2450
|
|
|
* @return ExpressionNode|null |
2451
|
|
|
*/ |
2452
|
|
|
protected function parseMediaFeature() |
2453
|
|
|
{ |
2454
|
|
|
$nodes = []; |
2455
|
|
|
$this->input->save(); |
2456
|
|
|
|
2457
|
|
|
do { |
2458
|
|
|
if ($e = $this->matchFuncs(['parseEntitiesKeyword', 'parseEntitiesVariable'])) { |
2459
|
|
|
$nodes[] = $e; |
2460
|
|
|
} elseif ($this->input->char('(')) { |
2461
|
|
|
$p = $this->parseProperty(); |
2462
|
|
|
$e = $this->parseValue(); |
2463
|
|
|
if ($this->input->char(')')) { |
2464
|
|
|
if ($p && $e) { |
2465
|
|
|
$nodes[] = new ParenNode( |
2466
|
|
|
new RuleNode($p, $e, null, null, $this->input->i, $this->context->currentFileInfo, true) |
2467
|
|
|
); |
2468
|
|
|
} elseif ($e) { |
2469
|
|
|
$nodes[] = new ParenNode($e); |
2470
|
|
|
} else { |
2471
|
|
|
$this->input->restore('Badly formed media feature definition'); |
2472
|
|
|
|
2473
|
|
|
return; |
2474
|
|
|
} |
2475
|
|
|
} else { |
2476
|
|
|
$this->input->restore("Missing closing ')'"); |
2477
|
|
|
|
2478
|
|
|
return; |
2479
|
|
|
} |
2480
|
|
|
} |
2481
|
|
|
} while ($e); |
2482
|
|
|
|
2483
|
|
|
$this->input->forget(); |
2484
|
|
|
|
2485
|
|
|
if ($nodes) { |
2486
|
|
|
return new ExpressionNode($nodes); |
2487
|
|
|
} |
2488
|
|
|
} |
2489
|
|
|
|
2490
|
|
|
/** |
2491
|
|
|
* A @plugin directive, used to import compiler extensions dynamically. `@plugin "lib"`;. |
2492
|
|
|
* |
2493
|
|
|
* @return ImportNode|null |
2494
|
|
|
* |
2495
|
|
|
* @throws ParserException |
2496
|
|
|
*/ |
2497
|
|
|
protected function parsePlugin() |
2498
|
|
|
{ |
2499
|
|
|
$index = $this->input->i; |
2500
|
|
|
$dir = $this->input->re('/\\G^@plugin?\s+/'); |
2501
|
|
|
if ($dir) { |
2502
|
|
|
$options = ['plugin' => true]; |
2503
|
|
|
if (($path = $this->matchFuncs(['parseEntitiesQuoted', 'parseEntitiesUrl']))) { |
2504
|
|
|
if (!$this->input->char(';')) { |
2505
|
|
|
$this->input->i = $index; |
2506
|
|
|
throw new ParserException('Missing semi-colon on plugin'); |
2507
|
|
|
} |
2508
|
|
|
|
2509
|
|
|
return new ImportNode($path, null, $options, $index, $this->context->currentFileInfo); |
2510
|
|
|
} else { |
2511
|
|
|
$this->input->i = $index; |
2512
|
|
|
throw new ParserException('Malformed plugin statement'); |
2513
|
|
|
} |
2514
|
|
|
} |
2515
|
|
|
} |
2516
|
|
|
|
2517
|
|
|
/** |
2518
|
|
|
* Parses the property. |
2519
|
|
|
* |
2520
|
|
|
* @return string|null |
2521
|
|
|
*/ |
2522
|
|
|
protected function parseProperty() |
2523
|
|
|
{ |
2524
|
|
|
if ($name = $this->input->re('/\\G(\*?-?[_a-zA-Z0-9-]+)\s*:/')) { |
2525
|
|
|
return $name[1]; |
2526
|
|
|
} |
2527
|
|
|
} |
2528
|
|
|
|
2529
|
|
|
/** |
2530
|
|
|
* Parses a rule terminator. |
2531
|
|
|
* |
2532
|
|
|
* @return string |
2533
|
|
|
*/ |
2534
|
|
|
protected function parseEnd() |
2535
|
|
|
{ |
2536
|
|
|
return ($end = $this->input->char(';')) ? $end : $this->input->peek('}'); |
2537
|
|
|
} |
2538
|
|
|
|
2539
|
|
|
/** |
2540
|
|
|
* Returns the context. |
2541
|
|
|
* |
2542
|
|
|
* @return Context |
2543
|
|
|
*/ |
2544
|
|
|
public function getContext() |
2545
|
|
|
{ |
2546
|
|
|
return $this->context; |
2547
|
|
|
} |
2548
|
|
|
|
2549
|
|
|
/** |
2550
|
|
|
* Set the current parser environment. |
2551
|
|
|
* |
2552
|
|
|
* @param Context $context |
2553
|
|
|
* |
2554
|
|
|
* @return $this |
2555
|
|
|
*/ |
2556
|
|
|
public function setContext(Context $context) |
2557
|
|
|
{ |
2558
|
|
|
$this->context = $context; |
2559
|
|
|
|
2560
|
|
|
return $this; |
2561
|
|
|
} |
2562
|
|
|
|
2563
|
|
|
/** |
2564
|
|
|
* Returns the importer. |
2565
|
|
|
* |
2566
|
|
|
* @return Importer |
2567
|
|
|
*/ |
2568
|
|
|
public function getImporter() |
2569
|
|
|
{ |
2570
|
|
|
return $this->importer; |
2571
|
|
|
} |
2572
|
|
|
|
2573
|
|
|
/** |
2574
|
|
|
* Set the importer. |
2575
|
|
|
* |
2576
|
|
|
* @param Importer $importer |
2577
|
|
|
* |
2578
|
|
|
* @return $this |
2579
|
|
|
*/ |
2580
|
|
|
public function setImporter(Importer $importer) |
2581
|
|
|
{ |
2582
|
|
|
$this->importer = $importer; |
2583
|
|
|
|
2584
|
|
|
return $this; |
2585
|
|
|
} |
2586
|
|
|
|
2587
|
|
|
/** |
2588
|
|
|
* Parse from a token, regexp or string, and move forward if match. |
2589
|
|
|
* |
2590
|
|
|
* @param array|string $token The token |
2591
|
|
|
* |
2592
|
|
|
* @return null|bool|object |
2593
|
|
|
*/ |
2594
|
|
|
protected function match($token) |
2595
|
|
|
{ |
2596
|
|
|
if (!is_array($token)) { |
2597
|
|
|
$token = [$token]; |
2598
|
|
|
} |
2599
|
|
|
|
2600
|
|
|
foreach ($token as $t) { |
2601
|
|
|
if (strlen($t) === 1) { |
2602
|
|
|
$match = $this->input->char($t); |
2603
|
|
|
} elseif ($t[0] !== '/') { |
2604
|
|
|
// Non-terminal, match using a function call |
2605
|
|
|
$match = $this->$t(); |
2606
|
|
|
} else { |
2607
|
|
|
$match = $this->input->re($t); |
2608
|
|
|
} |
2609
|
|
|
|
2610
|
|
|
if (null !== $match) { |
2611
|
|
|
return $match; |
2612
|
|
|
} |
2613
|
|
|
} |
2614
|
|
|
} |
2615
|
|
|
|
2616
|
|
|
/** |
2617
|
|
|
* Matches given functions. Returns the result of the first which returns |
2618
|
|
|
* any non null value. |
2619
|
|
|
* |
2620
|
|
|
* @param array $functions The array of functions to call |
2621
|
|
|
* |
2622
|
|
|
* @throws InvalidArgumentException If the function does not exist |
2623
|
|
|
* |
2624
|
|
|
* @return Node|mixed |
2625
|
|
|
*/ |
2626
|
|
|
protected function matchFuncs(array $functions) |
2627
|
|
|
{ |
2628
|
|
|
foreach ($functions as $func) { |
2629
|
|
|
if (!method_exists($this, $func)) { |
2630
|
|
|
throw new InvalidArgumentException(sprintf('The function "%s" does not exist.', $func)); |
2631
|
|
|
} |
2632
|
|
|
$match = $this->$func(); |
2633
|
|
|
if ($match !== null) { |
2634
|
|
|
return $match; |
2635
|
|
|
} |
2636
|
|
|
} |
2637
|
|
|
} |
2638
|
|
|
|
2639
|
|
|
/** |
2640
|
|
|
* Expects a string to be present at the current position. |
2641
|
|
|
* |
2642
|
|
|
* @param string $token The single character |
2643
|
|
|
* @param string $message The error message for the exception |
2644
|
|
|
* |
2645
|
|
|
* @return Node|null |
2646
|
|
|
* |
2647
|
|
|
* @throws ParserException If the expected token does not match |
2648
|
|
|
*/ |
2649
|
|
|
protected function expect($token, $message = null) |
2650
|
|
|
{ |
2651
|
|
|
$result = $this->match($token); |
2652
|
|
|
if (!$result) { |
2653
|
|
|
throw new ParserException( |
2654
|
|
|
$message ? $message : |
2655
|
|
|
sprintf( |
2656
|
|
|
'Expected \'%s\' got \'%s\' at index %s', |
2657
|
|
|
$token, |
2658
|
|
|
$this->input->currentChar(), |
2659
|
|
|
$this->input->i |
2660
|
|
|
), |
2661
|
|
|
$this->input->i, $this->context->currentFileInfo |
2662
|
|
|
); |
2663
|
|
|
} |
2664
|
|
|
|
2665
|
|
|
return $result; |
2666
|
|
|
} |
2667
|
|
|
|
2668
|
|
|
/** |
2669
|
|
|
* Returns the debug information. |
2670
|
|
|
* |
2671
|
|
|
* @param int $index The index |
2672
|
|
|
* |
2673
|
|
|
* @return \ILess\DebugInfo |
2674
|
|
|
*/ |
2675
|
|
|
protected function getDebugInfo($index) |
2676
|
|
|
{ |
2677
|
|
|
list($lineNumber) = Util::getLocation($this->input->getInput(), $index); |
2678
|
|
|
|
2679
|
|
|
return new DebugInfo($this->context->currentFileInfo->filename, $lineNumber); |
2680
|
|
|
} |
2681
|
|
|
} |
2682
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.