PluginProbe
MaxButtons – Create buttons / 6.3
MaxButtons – Create buttons v6.3
6.23 6.24 6.25 6.26 6.26.1 6.27 6.28 6.3 6.4 6.5 6.6 6.7 6.8 6.9 7.0 7.1 7.1.1 7.1.2 7.1.3 7.10 7.11 7.13 7.13.1 7.13.2 7.13.3 All 100 releases
maxbuttons / assets / libraries / scssphp / src / Parser.php

Parser.php in MaxButtons – Create buttons 6.3, at assets/libraries/scssphp/src/Parser.php

2,370 lines 55.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SCSSPHP
4 *
5 * @copyright 2012-2015 Leaf Corcoran
6 *
7 * @license http://opensource.org/licenses/MIT MIT
8 *
9 * @link http://leafo.github.io/scssphp
10 */
11
12 namespace Leafo\ScssPhp;
13
14 use Leafo\ScssPhp\Compiler;
15
16 /**
17 * SCSS parser
18 *
19 * @author Leaf Corcoran <leafot@gmail.com>
20 */
21 class Parser
22 {
23 const SOURCE_POSITION = -1;
24 const SOURCE_PARSER = -2;
25
26 /**
27 * @var array
28 */
29 protected static $precedence = array(
30 '=' => 0,
31 'or' => 1,
32 'and' => 2,
33 '==' => 3,
34 '!=' => 3,
35 '<=' => 4,
36 '>=' => 4,
37 '<' => 4,
38 '>' => 4,
39 '+' => 5,
40 '-' => 5,
41 '*' => 6,
42 '/' => 6,
43 '%' => 6,
44 );
45
46 /**
47 * @var array
48 */
49 protected static $operators = array(
50 '+',
51 '-',
52 '*',
53 '/',
54 '%',
55 '==',
56 '!=',
57 '<=',
58 '>=',
59 '<',
60 '>',
61 'and',
62 'or',
63 );
64
65 protected static $operatorStr;
66 protected static $whitePattern;
67 protected static $commentMulti;
68
69 protected static $commentSingle = '//';
70 protected static $commentMultiLeft = '/*';
71 protected static $commentMultiRight = '*/';
72
73 private $sourceName;
74 private $rootParser;
75 private $charset;
76 private $count;
77 private $env;
78 private $inParens;
79 private $eatWhiteDefault;
80 private $buffer;
81
82 /**
83 * Constructor
84 *
85 * @param string $sourceName
86 * @param boolean $rootParser
87 */
88 public function __construct($sourceName = null, $rootParser = true)
89 {
90 $this->sourceName = $sourceName ?: '(stdin)';
91 $this->rootParser = $rootParser;
92 $this->charset = null;
93
94 if (empty(self::$operatorStr)) {
95 self::$operatorStr = $this->makeOperatorStr(self::$operators);
96
97 $commentSingle = $this->pregQuote(self::$commentSingle);
98 $commentMultiLeft = $this->pregQuote(self::$commentMultiLeft);
99 $commentMultiRight = $this->pregQuote(self::$commentMultiRight);
100 self::$commentMulti = $commentMultiLeft . '.*?' . $commentMultiRight;
101 self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentMulti . ')\s*|\s+/Ais';
102 }
103 }
104
105 /**
106 * Make operator regex
107 *
108 * @param array $operators
109 *
110 * @return string
111 */
112 protected static function makeOperatorStr($operators)
113 {
114 return '('
115 . implode('|', array_map(array('Leafo\ScssPhp\Parser', 'pregQuote'), $operators))
116 . ')';
117 }
118
119 /**
120 * Parser buffer
121 *
122 * @param string $buffer;
123 *
124 * @return \stdClass
125 */
126 public function parse($buffer)
127 {
128 $this->count = 0;
129 $this->env = null;
130 $this->inParens = false;
131 $this->eatWhiteDefault = true;
132 $this->buffer = $buffer;
133
134 $this->pushBlock(null); // root block
135
136 $this->whitespace();
137 $this->pushBlock(null);
138 $this->popBlock();
139
140 while ($this->parseChunk()) {
141 ;
142 }
143
144 if ($this->count !== strlen($this->buffer)) {
145 $this->throwParseError();
146 }
147
148 if (! empty($this->env->parent)) {
149 $this->throwParseError('unclosed block');
150 }
151
152 if ($this->charset) {
153 array_unshift($this->env->children, $this->charset);
154 }
155
156 $this->env->isRoot = true;
157
158 return $this->env;
159 }
160
161 /**
162 * Parse a value or value list
163 *
164 * @param string $buffer
165 * @param string $out
166 *
167 * @return boolean
168 */
169 public function parseValue($buffer, &$out)
170 {
171 $this->count = 0;
172 $this->env = null;
173 $this->inParens = false;
174 $this->eatWhiteDefault = true;
175 $this->buffer = (string) $buffer;
176
177 return $this->valueList($out);
178 }
179
180 /**
181 * Parse a selector or selector list
182 *
183 * @param string $buffer
184 * @param string $out
185 *
186 * @return boolean
187 */
188 public function parseSelector($buffer, &$out)
189 {
190 $this->count = 0;
191 $this->env = null;
192 $this->inParens = false;
193 $this->eatWhiteDefault = true;
194 $this->buffer = (string) $buffer;
195
196 return $this->selectors($out);
197 }
198
199 /**
200 * Parse a single chunk off the head of the buffer and append it to the
201 * current parse environment.
202 *
203 * Returns false when the buffer is empty, or when there is an error.
204 *
205 * This function is called repeatedly until the entire document is
206 * parsed.
207 *
208 * This parser is most similar to a recursive descent parser. Single
209 * functions represent discrete grammatical rules for the language, and
210 * they are able to capture the text that represents those rules.
211 *
212 * Consider the function Compiler::keyword(). (All parse functions are
213 * structured the same.)
214 *
215 * The function takes a single reference argument. When calling the
216 * function it will attempt to match a keyword on the head of the buffer.
217 * If it is successful, it will place the keyword in the referenced
218 * argument, advance the position in the buffer, and return true. If it
219 * fails then it won't advance the buffer and it will return false.
220 *
221 * All of these parse functions are powered by Compiler::match(), which behaves
222 * the same way, but takes a literal regular expression. Sometimes it is
223 * more convenient to use match instead of creating a new function.
224 *
225 * Because of the format of the functions, to parse an entire string of
226 * grammatical rules, you can chain them together using &&.
227 *
228 * But, if some of the rules in the chain succeed before one fails, then
229 * the buffer position will be left at an invalid state. In order to
230 * avoid this, Compiler::seek() is used to remember and set buffer positions.
231 *
232 * Before parsing a chain, use $s = $this->seek() to remember the current
233 * position into $s. Then if a chain fails, use $this->seek($s) to
234 * go back where we started.
235 *
236 * @return boolean
237 */
238 protected function parseChunk()
239 {
240 $s = $this->seek();
241
242 // the directives
243 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
244 if ($this->literal('@at-root') &&
245 ($this->selectors($selector) || true) &&
246 ($this->map($with) || true) &&
247 $this->literal('{')
248 ) {
249 $atRoot = $this->pushSpecialBlock('at-root', $s);
250 $atRoot->selector = $selector;
251 $atRoot->with = $with;
252
253 return true;
254 }
255
256 $this->seek($s);
257
258 if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) {
259 $media = $this->pushSpecialBlock('media', $s);
260 $media->queryList = $mediaQueryList[2];
261
262 return true;
263 }
264
265 $this->seek($s);
266
267 if ($this->literal('@mixin') &&
268 $this->keyword($mixinName) &&
269 ($this->argumentDef($args) || true) &&
270 $this->literal('{')
271 ) {
272 $mixin = $this->pushSpecialBlock('mixin', $s);
273 $mixin->name = $mixinName;
274 $mixin->args = $args;
275
276 return true;
277 }
278
279 $this->seek($s);
280
281 if ($this->literal('@include') &&
282 $this->keyword($mixinName) &&
283 ($this->literal('(') &&
284 ($this->argValues($argValues) || true) &&
285 $this->literal(')') || true) &&
286 ($this->end() ||
287 $this->literal('{') && $hasBlock = true)
288 ) {
289 $child = array('include',
290 $mixinName, isset($argValues) ? $argValues : null, null);
291
292 if (! empty($hasBlock)) {
293 $include = $this->pushSpecialBlock('include', $s);
294 $include->child = $child;
295 } else {
296 $this->append($child, $s);
297 }
298
299 return true;
300 }
301
302 $this->seek($s);
303
304 if ($this->literal('@import') &&
305 $this->valueList($importPath) &&
306 $this->end()
307 ) {
308 $this->append(array('import', $importPath), $s);
309
310 return true;
311 }
312
313 $this->seek($s);
314
315 if ($this->literal('@import') &&
316 $this->url($importPath) &&
317 $this->end()
318 ) {
319 $this->append(array('import', $importPath), $s);
320
321 return true;
322 }
323
324 $this->seek($s);
325
326 if ($this->literal('@extend') &&
327 $this->selectors($selector) &&
328 $this->end()
329 ) {
330 $this->append(array('extend', $selector), $s);
331
332 return true;
333 }
334
335 $this->seek($s);
336
337 if ($this->literal('@function') &&
338 $this->keyword($fnName) &&
339 $this->argumentDef($args) &&
340 $this->literal('{')
341 ) {
342 $func = $this->pushSpecialBlock('function', $s);
343 $func->name = $fnName;
344 $func->args = $args;
345
346 return true;
347 }
348
349 $this->seek($s);
350
351 if ($this->literal('@return') && $this->valueList($retVal) && $this->end()) {
352 $this->append(array('return', $retVal), $s);
353
354 return true;
355 }
356
357 $this->seek($s);
358
359 if ($this->literal('@each') &&
360 $this->genericList($varNames, 'variable', ',', false) &&
361 $this->literal('in') &&
362 $this->valueList($list) &&
363 $this->literal('{')
364 ) {
365 $each = $this->pushSpecialBlock('each', $s);
366
367 foreach ($varNames[2] as $varName) {
368 $each->vars[] = $varName[1];
369 }
370
371 $each->list = $list;
372
373 return true;
374 }
375
376 $this->seek($s);
377
378 if ($this->literal('@while') &&
379 $this->expression($cond) &&
380 $this->literal('{')
381 ) {
382 $while = $this->pushSpecialBlock('while', $s);
383 $while->cond = $cond;
384
385 return true;
386 }
387
388 $this->seek($s);
389
390 if ($this->literal('@for') &&
391 $this->variable($varName) &&
392 $this->literal('from') &&
393 $this->expression($start) &&
394 ($this->literal('through') ||
395 ($forUntil = true && $this->literal('to'))) &&
396 $this->expression($end) &&
397 $this->literal('{')
398 ) {
399 $for = $this->pushSpecialBlock('for', $s);
400 $for->var = $varName[1];
401 $for->start = $start;
402 $for->end = $end;
403 $for->until = isset($forUntil);
404
405 return true;
406 }
407
408 $this->seek($s);
409
410 if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) {
411 $if = $this->pushSpecialBlock('if', $s);
412 $if->cond = $cond;
413 $if->cases = array();
414
415 return true;
416 }
417
418 $this->seek($s);
419
420 if ($this->literal('@debug') &&
421 $this->valueList($value) &&
422 $this->end()
423 ) {
424 $this->append(array('debug', $value), $s);
425
426 return true;
427 }
428
429 $this->seek($s);
430
431 if ($this->literal('@warn') &&
432 $this->valueList($value) &&
433 $this->end()
434 ) {
435 $this->append(array('warn', $value), $s);
436
437 return true;
438 }
439
440 $this->seek($s);
441
442 if ($this->literal('@error') &&
443 $this->valueList($value) &&
444 $this->end()
445 ) {
446 $this->append(array('error', $value), $s);
447
448 return true;
449 }
450
451 $this->seek($s);
452
453 if ($this->literal('@content') && $this->end()) {
454 $this->append(array('mixin_content'), $s);
455
456 return true;
457 }
458
459 $this->seek($s);
460
461 $last = $this->last();
462
463 if (isset($last) && $last[0] === 'if') {
464 list(, $if) = $last;
465
466 if ($this->literal('@else')) {
467 if ($this->literal('{')) {
468 $else = $this->pushSpecialBlock('else', $s);
469 } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) {
470 $else = $this->pushSpecialBlock('elseif', $s);
471 $else->cond = $cond;
472 }
473
474 if (isset($else)) {
475 $else->dontAppend = true;
476 $if->cases[] = $else;
477
478 return true;
479 }
480 }
481
482 $this->seek($s);
483 }
484
485 // only retain the first @charset directive encountered
486 if ($this->literal('@charset') &&
487 $this->valueList($charset) &&
488 $this->end()
489 ) {
490 if (! isset($this->charset)) {
491 $statement = array('charset', $charset);
492
493 $statement[self::SOURCE_POSITION] = $s;
494
495 if (! $this->rootParser) {
496 $statement[self::SOURCE_PARSER] = $this;
497 }
498
499 $this->charset = $statement;
500 }
501
502 return true;
503 }
504
505 $this->seek($s);
506
507 // doesn't match built in directive, do generic one
508 if ($this->literal('@', false) &&
509 $this->keyword($dirName) &&
510 ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
511 $this->literal('{')
512 ) {
513 $directive = $this->pushSpecialBlock('directive', $s);
514 $directive->name = $dirName;
515
516 if (isset($dirValue)) {
517 $directive->value = $dirValue;
518 }
519
520 return true;
521 }
522
523 $this->seek($s);
524
525 return false;
526 }
527
528 // property shortcut
529 // captures most properties before having to parse a selector
530 if ($this->keyword($name, false) &&
531 $this->literal(': ') &&
532 $this->valueList($value) &&
533 $this->end()
534 ) {
535 $name = array('string', '', array($name));
536 $this->append(array('assign', $name, $value), $s);
537
538 return true;
539 }
540
541 $this->seek($s);
542
543 // variable assigns
544 if ($this->variable($name) &&
545 $this->literal(':') &&
546 $this->valueList($value) &&
547 $this->end()
548 ) {
549 // check for '!flag'
550 $assignmentFlag = $this->stripAssignmentFlag($value);
551 $this->append(array('assign', $name, $value, $assignmentFlag), $s);
552
553 return true;
554 }
555
556 $this->seek($s);
557
558 // misc
559 if ($this->literal('-->')) {
560 return true;
561 }
562
563 // opening css block
564 if ($this->selectors($selectors) && $this->literal('{')) {
565 $b = $this->pushBlock($selectors, $s);
566
567 return true;
568 }
569
570 $this->seek($s);
571
572 // property assign, or nested assign
573 if ($this->propertyName($name) && $this->literal(':')) {
574 $foundSomething = false;
575
576 if ($this->valueList($value)) {
577 $this->append(array('assign', $name, $value), $s);
578 $foundSomething = true;
579 }
580
581 if ($this->literal('{')) {
582 $propBlock = $this->pushSpecialBlock('nestedprop', $s);
583 $propBlock->prefix = $name;
584 $foundSomething = true;
585 } elseif ($foundSomething) {
586 $foundSomething = $this->end();
587 }
588
589 if ($foundSomething) {
590 return true;
591 }
592 }
593
594 $this->seek($s);
595
596 // closing a block
597 if ($this->literal('}')) {
598 $block = $this->popBlock();
599
600 if (isset($block->type) && $block->type === 'include') {
601 $include = $block->child;
602 unset($block->child);
603 $include[3] = $block;
604 $this->append($include, $s);
605 } elseif (empty($block->dontAppend)) {
606 $type = isset($block->type) ? $block->type : 'block';
607 $this->append(array($type, $block), $s);
608 }
609
610 return true;
611 }
612
613 // extra stuff
614 if ($this->literal(';') ||
615 $this->literal('<!--')
616 ) {
617 return true;
618 }
619
620 return false;
621 }
622
623 /**
624 * Strip assignment flag from the list
625 *
626 * @param array $value
627 *
628 * @return string
629 */
630 protected function stripAssignmentFlag(&$value)
631 {
632 $token = &$value;
633
634 for ($token = &$value; $token[0] === 'list' && ($s = count($token[2])); $token = &$lastNode) {
635 $lastNode = &$token[2][$s - 1];
636
637 if ($lastNode[0] === 'keyword' && in_array($lastNode[1], array('!default', '!global'))) {
638 array_pop($token[2]);
639
640 $token = $this->flattenList($token);
641
642 return $lastNode[1];
643 }
644 }
645
646 return false;
647 }
648
649 /**
650 * Match literal string
651 *
652 * @param string $what
653 * @param boolean $eatWhitespace
654 *
655 * @return boolean
656 */
657 protected function literal($what, $eatWhitespace = null)
658 {
659 if (! isset($eatWhitespace)) {
660 $eatWhitespace = $this->eatWhiteDefault;
661 }
662
663 // shortcut on single letter
664 if (! isset($what[1]) && isset($this->buffer[$this->count])) {
665 if ($this->buffer[$this->count] === $what) {
666 if (! $eatWhitespace) {
667 $this->count++;
668
669 return true;
670 }
671 // goes below...
672 } else {
673 return false;
674 }
675 }
676
677 return $this->match($this->pregQuote($what), $m, $eatWhitespace);
678 }
679
680 /**
681 * Push block onto parse tree
682 *
683 * @param array $selectors
684 * @param integer $pos
685 *
686 * @return \stdClass
687 */
688 protected function pushBlock($selectors, $pos = 0)
689 {
690 $b = new \stdClass;
691 $b->parent = $this->env;
692
693 $b->sourcePosition = $pos;
694 $b->sourceParser = $this;
695 $b->selectors = $selectors;
696 $b->comments = array();
697
698 if (! $this->env) {
699 $b->children = array();
700 } elseif (empty($this->env->children)) {
701 $this->env->children = $this->env->comments;
702 $b->children = array();
703 $this->env->comments = array();
704 } else {
705 $b->children = $this->env->comments;
706 $this->env->comments = array();
707 }
708
709 $this->env = $b;
710
711 return $b;
712 }
713
714 /**
715 * Push special (named) block onto parse tree
716 *
717 * @param string $type
718 * @param integer $pos
719 *
720 * @return \stdClass
721 */
722 protected function pushSpecialBlock($type, $pos)
723 {
724 $block = $this->pushBlock(null, $pos);
725 $block->type = $type;
726
727 return $block;
728 }
729
730 /**
731 * Pop scope and return last block
732 *
733 * @return \stdClass
734 *
735 * @throws \Exception
736 */
737 protected function popBlock()
738 {
739 $block = $this->env;
740
741 if (empty($block->parent)) {
742 $this->throwParseError('unexpected }');
743 }
744
745 $this->env = $block->parent;
746 unset($block->parent);
747
748 $comments = $block->comments;
749 if (count($comments)) {
750 $this->env->comments = $comments;
751 unset($block->comments);
752 }
753
754 return $block;
755 }
756
757 /**
758 * Append comment to current block
759 *
760 * @param array $comment
761 */
762 protected function appendComment($comment)
763 {
764 $comment[1] = substr(preg_replace(array('/^\s+/m', '/^(.)/m'), array('', ' \1'), $comment[1]), 1);
765
766 $this->env->comments[] = $comment;
767 }
768
769 /**
770 * Append statement to current block
771 *
772 * @param array $statement
773 * @param integer $pos
774 */
775 protected function append($statement, $pos = null)
776 {
777 if ($pos !== null) {
778 $statement[self::SOURCE_POSITION] = $pos;
779
780 if (! $this->rootParser) {
781 $statement[self::SOURCE_PARSER] = $this;
782 }
783 }
784
785 $this->env->children[] = $statement;
786
787 $comments = $this->env->comments;
788
789 if (count($comments)) {
790 $this->env->children = array_merge($this->env->children, $comments);
791 $this->env->comments = array();
792 }
793 }
794
795 /**
796 * Returns last child was appended
797 *
798 * @return array|null
799 */
800 protected function last()
801 {
802 $i = count($this->env->children) - 1;
803
804 if (isset($this->env->children[$i])) {
805 return $this->env->children[$i];
806 }
807 }
808
809 /**
810 * Parse media query list
811 *
812 * @param array $out
813 *
814 * @return boolean
815 */
816 protected function mediaQueryList(&$out)
817 {
818 return $this->genericList($out, 'mediaQuery', ',', false);
819 }
820
821 /**
822 * Parse media query
823 *
824 * @param array $out
825 *
826 * @return boolean
827 */
828 protected function mediaQuery(&$out)
829 {
830 $s = $this->seek();
831
832 $expressions = null;
833 $parts = array();
834
835 if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) &&
836 $this->mixedKeyword($mediaType)
837 ) {
838 $prop = array('mediaType');
839
840 if (isset($only)) {
841 $prop[] = array('keyword', 'only');
842 }
843
844 if (isset($not)) {
845 $prop[] = array('keyword', 'not');
846 }
847
848 $media = array('list', '', array());
849
850 foreach ((array)$mediaType as $type) {
851 if (is_array($type)) {
852 $media[2][] = $type;
853 } else {
854 $media[2][] = array('keyword', $type);
855 }
856 }
857
858 $prop[] = $media;
859 $parts[] = $prop;
860 }
861
862 if (empty($parts) || $this->literal('and')) {
863 $this->genericList($expressions, 'mediaExpression', 'and', false);
864
865 if (is_array($expressions)) {
866 $parts = array_merge($parts, $expressions[2]);
867 }
868 }
869
870 $out = $parts;
871
872 return true;
873 }
874
875 /**
876 * Parse media expression
877 *
878 * @param array $out
879 *
880 * @return boolean
881 */
882 protected function mediaExpression(&$out)
883 {
884 $s = $this->seek();
885 $value = null;
886
887 if ($this->literal('(') &&
888 $this->expression($feature) &&
889 ($this->literal(':') && $this->expression($value) || true) &&
890 $this->literal(')')
891 ) {
892 $out = array('mediaExp', $feature);
893
894 if ($value) {
895 $out[] = $value;
896 }
897
898 return true;
899 }
900
901 $this->seek($s);
902
903 return false;
904 }
905
906 /**
907 * Parse argument values
908 *
909 * @param array $out
910 *
911 * @return boolean
912 */
913 protected function argValues(&$out)
914 {
915 if ($this->genericList($list, 'argValue', ',', false)) {
916 $out = $list[2];
917
918 return true;
919 }
920
921 return false;
922 }
923
924 /**
925 * Parse argument value
926 *
927 * @param array $out
928 *
929 * @return boolean
930 */
931 protected function argValue(&$out)
932 {
933 $s = $this->seek();
934
935 $keyword = null;
936
937 if (! $this->variable($keyword) || ! $this->literal(':')) {
938 $this->seek($s);
939 $keyword = null;
940 }
941
942 if ($this->genericList($value, 'expression')) {
943 $out = array($keyword, $value, false);
944 $s = $this->seek();
945
946 if ($this->literal('...')) {
947 $out[2] = true;
948 } else {
949 $this->seek($s);
950 }
951
952 return true;
953 }
954
955 return false;
956 }
957
958 /**
959 * Parse list
960 *
961 * @param string $out
962 *
963 * @return boolean
964 */
965 protected function valueList(&$out)
966 {
967 return $this->genericList($out, 'spaceList', ',');
968 }
969
970 /**
971 * Parse space list
972 *
973 * @param array $out
974 *
975 * @return boolean
976 */
977 protected function spaceList(&$out)
978 {
979 return $this->genericList($out, 'expression');
980 }
981
982 /**
983 * Parse generic list
984 *
985 * @param array $out
986 * @param callable $parseItem
987 * @param string $delim
988 * @param boolean $flatten
989 *
990 * @return boolean
991 */
992 protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
993 {
994 $s = $this->seek();
995 $items = array();
996
997 while ($this->$parseItem($value)) {
998 $items[] = $value;
999
1000 if ($delim) {
1001 if (! $this->literal($delim)) {
1002 break;
1003 }
1004 }
1005 }
1006
1007 if (count($items) === 0) {
1008 $this->seek($s);
1009
1010 return false;
1011 }
1012
1013 if ($flatten && count($items) === 1) {
1014 $out = $items[0];
1015 } else {
1016 $out = array('list', $delim, $items);
1017 }
1018
1019 return true;
1020 }
1021
1022 /**
1023 * Parse expression
1024 *
1025 * @param array $out
1026 *
1027 * @return boolean
1028 */
1029 protected function expression(&$out)
1030 {
1031 $s = $this->seek();
1032
1033 if ($this->literal('(')) {
1034 if ($this->literal(')')) {
1035 $out = array('list', '', array());
1036
1037 return true;
1038 }
1039
1040 if ($this->valueList($out) && $this->literal(')') && $out[0] === 'list') {
1041 return true;
1042 }
1043
1044 $this->seek($s);
1045
1046 if ($this->map($out)) {
1047 return true;
1048 }
1049
1050 $this->seek($s);
1051 }
1052
1053 if ($this->value($lhs)) {
1054 $out = $this->expHelper($lhs, 0);
1055
1056 return true;
1057 }
1058
1059 return false;
1060 }
1061
1062 /**
1063 * Parse left-hand side of subexpression
1064 *
1065 * @param array $lhs
1066 * @param integer $minP
1067 *
1068 * @return array
1069 */
1070 protected function expHelper($lhs, $minP)
1071 {
1072 $opstr = self::$operatorStr;
1073
1074 $ss = $this->seek();
1075 $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1076 ctype_space($this->buffer[$this->count - 1]);
1077
1078 while ($this->match($opstr, $m, false) && self::$precedence[$m[1]] >= $minP) {
1079 $whiteAfter = isset($this->buffer[$this->count]) &&
1080 ctype_space($this->buffer[$this->count]);
1081 $varAfter = isset($this->buffer[$this->count]) &&
1082 $this->buffer[$this->count] === '$';
1083
1084 $this->whitespace();
1085
1086 $op = $m[1];
1087
1088 // don't turn negative numbers into expressions
1089 if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) {
1090 break;
1091 }
1092
1093 if (! $this->value($rhs)) {
1094 break;
1095 }
1096
1097 // peek and see if rhs belongs to next operator
1098 if ($this->peek($opstr, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
1099 $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
1100 }
1101
1102 $lhs = array('exp', $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter);
1103 $ss = $this->seek();
1104 $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1105 ctype_space($this->buffer[$this->count - 1]);
1106 }
1107
1108 $this->seek($ss);
1109
1110 return $lhs;
1111 }
1112
1113 /**
1114 * Parse value
1115 *
1116 * @param array $out
1117 *
1118 * @return boolean
1119 */
1120 protected function value(&$out)
1121 {
1122 $s = $this->seek();
1123
1124 if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) {
1125 $out = array('unary', 'not', $inner, $this->inParens);
1126
1127 return true;
1128 }
1129
1130 $this->seek($s);
1131
1132 if ($this->literal('not', false) && $this->parenValue($inner)) {
1133 $out = array('unary', 'not', $inner, $this->inParens);
1134
1135 return true;
1136 }
1137
1138 $this->seek($s);
1139
1140 if ($this->literal('+') && $this->value($inner)) {
1141 $out = array('unary', '+', $inner, $this->inParens);
1142
1143 return true;
1144 }
1145
1146 $this->seek($s);
1147
1148 // negation
1149 if ($this->literal('-', false) &&
1150 ($this->variable($inner) ||
1151 $this->unit($inner) ||
1152 $this->parenValue($inner))
1153 ) {
1154 $out = array('unary', '-', $inner, $this->inParens);
1155
1156 return true;
1157 }
1158
1159 $this->seek($s);
1160
1161 if ($this->parenValue($out) ||
1162 $this->interpolation($out) ||
1163 $this->variable($out) ||
1164 $this->color($out) ||
1165 $this->unit($out) ||
1166 $this->string($out) ||
1167 $this->func($out) ||
1168 $this->progid($out)
1169 ) {
1170 return true;
1171 }
1172
1173 if ($this->keyword($keyword)) {
1174 if ($keyword === 'null') {
1175 $out = array('null');
1176 } else {
1177 $out = array('keyword', $keyword);
1178 }
1179
1180 return true;
1181 }
1182
1183 return false;
1184 }
1185
1186 /**
1187 * Parse parenthesized value
1188 *
1189 * @param array $out
1190 *
1191 * @return boolean
1192 */
1193 protected function parenValue(&$out)
1194 {
1195 $s = $this->seek();
1196
1197 $inParens = $this->inParens;
1198
1199 if ($this->literal('(')) {
1200 if ($this->literal(')')) {
1201 $out = array('list', '', array());
1202
1203 return true;
1204 }
1205
1206 $this->inParens = true;
1207
1208 if ($this->expression($exp) && $this->literal(')')) {
1209 $out = $exp;
1210 $this->inParens = $inParens;
1211
1212 return true;
1213 }
1214 }
1215
1216 $this->inParens = $inParens;
1217 $this->seek($s);
1218
1219 return false;
1220 }
1221
1222 /**
1223 * Parse "progid:"
1224 *
1225 * @param array $out
1226 *
1227 * @return boolean
1228 */
1229 protected function progid(&$out)
1230 {
1231 $s = $this->seek();
1232
1233 if ($this->literal('progid:', false) &&
1234 $this->openString('(', $fn) &&
1235 $this->literal('(')
1236 ) {
1237 $this->openString(')', $args, '(');
1238
1239 if ($this->literal(')')) {
1240 $out = array('string', '', array(
1241 'progid:', $fn, '(', $args, ')'
1242 ));
1243
1244 return true;
1245 }
1246 }
1247
1248 $this->seek($s);
1249
1250 return false;
1251 }
1252
1253 /**
1254 * Parse function call
1255 *
1256 * @param array $out
1257 *
1258 * @return boolean
1259 */
1260 protected function func(&$func)
1261 {
1262 $s = $this->seek();
1263
1264 if ($this->keyword($name, false) &&
1265 $this->literal('(')
1266 ) {
1267 if ($name === 'alpha' && $this->argumentList($args)) {
1268 $func = array('function', $name, array('string', '', $args));
1269
1270 return true;
1271 }
1272
1273 if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
1274 $ss = $this->seek();
1275
1276 if ($this->argValues($args) && $this->literal(')')) {
1277 $func = array('fncall', $name, $args);
1278
1279 return true;
1280 }
1281
1282 $this->seek($ss);
1283 }
1284
1285 if (($this->openString(')', $str, '(') || true ) &&
1286 $this->literal(')')
1287 ) {
1288 $args = array();
1289
1290 if (! empty($str)) {
1291 $args[] = array(null, array('string', '', array($str)));
1292 }
1293
1294 $func = array('fncall', $name, $args);
1295
1296 return true;
1297 }
1298 }
1299
1300 $this->seek($s);
1301
1302 return false;
1303 }
1304
1305 /**
1306 * Parse function call argument list
1307 *
1308 * @param array $out
1309 *
1310 * @return boolean
1311 */
1312 protected function argumentList(&$out)
1313 {
1314 $s = $this->seek();
1315 $this->literal('(');
1316
1317 $args = array();
1318
1319 while ($this->keyword($var)) {
1320 $ss = $this->seek();
1321
1322 if ($this->literal('=') && $this->expression($exp)) {
1323 $args[] = array('string', '', array($var . '='));
1324 $arg = $exp;
1325 } else {
1326 break;
1327 }
1328
1329 $args[] = $arg;
1330
1331 if (! $this->literal(',')) {
1332 break;
1333 }
1334
1335 $args[] = array('string', '', array(', '));
1336 }
1337
1338 if (! $this->literal(')') || ! count($args)) {
1339 $this->seek($s);
1340
1341 return false;
1342 }
1343
1344 $out = $args;
1345
1346 return true;
1347 }
1348
1349 /**
1350 * Parse mixin/function definition argument list
1351 *
1352 * @param array $out
1353 *
1354 * @return boolean
1355 */
1356 protected function argumentDef(&$out)
1357 {
1358 $s = $this->seek();
1359 $this->literal('(');
1360
1361 $args = array();
1362
1363 while ($this->variable($var)) {
1364 $arg = array($var[1], null, false);
1365
1366 $ss = $this->seek();
1367
1368 if ($this->literal(':') && $this->genericList($defaultVal, 'expression')) {
1369 $arg[1] = $defaultVal;
1370 } else {
1371 $this->seek($ss);
1372 }
1373
1374 $ss = $this->seek();
1375
1376 if ($this->literal('...')) {
1377 $sss = $this->seek();
1378
1379 if (! $this->literal(')')) {
1380 $this->throwParseError('... has to be after the final argument');
1381 }
1382
1383 $arg[2] = true;
1384 $this->seek($sss);
1385 } else {
1386 $this->seek($ss);
1387 }
1388
1389 $args[] = $arg;
1390
1391 if (! $this->literal(',')) {
1392 break;
1393 }
1394 }
1395
1396 if (! $this->literal(')')) {
1397 $this->seek($s);
1398
1399 return false;
1400 }
1401
1402 $out = $args;
1403
1404 return true;
1405 }
1406
1407 /**
1408 * Parse map
1409 *
1410 * @param array $out
1411 *
1412 * @return boolean
1413 */
1414 protected function map(&$out)
1415 {
1416 $s = $this->seek();
1417
1418 if (! $this->literal('(')) {
1419 return false;
1420 }
1421
1422 $keys = array();
1423 $values = array();
1424
1425 while ($this->genericList($key, 'expression') && $this->literal(':') &&
1426 $this->genericList($value, 'expression')
1427 ) {
1428 $keys[] = $key;
1429 $values[] = $value;
1430
1431 if (! $this->literal(',')) {
1432 break;
1433 }
1434 }
1435
1436 if (! count($keys) || ! $this->literal(')')) {
1437 $this->seek($s);
1438
1439 return false;
1440 }
1441
1442 $out = array('map', $keys, $values);
1443
1444 return true;
1445 }
1446
1447 /**
1448 * Parse color
1449 *
1450 * @param array $out
1451 *
1452 * @return boolean
1453 */
1454 protected function color(&$out)
1455 {
1456 $color = array('color');
1457
1458 if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
1459 if (isset($m[3])) {
1460 $num = $m[3];
1461 $width = 16;
1462 } else {
1463 $num = $m[2];
1464 $width = 256;
1465 }
1466
1467 $num = hexdec($num);
1468
1469 foreach (array(3, 2, 1) as $i) {
1470 $t = $num % $width;
1471 $num /= $width;
1472
1473 $color[$i] = $t * (256/$width) + $t * floor(16/$width);
1474 }
1475
1476 $out = $color;
1477
1478 return true;
1479 }
1480
1481 return false;
1482 }
1483
1484 /**
1485 * Parse number with unit
1486 *
1487 * @param array $out
1488 *
1489 * @return boolean
1490 */
1491 protected function unit(&$unit)
1492 {
1493 if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
1494 $unit = array('number', $m[1], empty($m[3]) ? '' : $m[3]);
1495
1496 return true;
1497 }
1498
1499 return false;
1500 }
1501
1502 /**
1503 * Parse string
1504 *
1505 * @param array $out
1506 *
1507 * @return boolean
1508 */
1509 protected function string(&$out)
1510 {
1511 $s = $this->seek();
1512
1513 if ($this->literal('"', false)) {
1514 $delim = '"';
1515 } elseif ($this->literal('\'', false)) {
1516 $delim = '\'';
1517 } else {
1518 return false;
1519 }
1520
1521 $content = array();
1522 $oldWhite = $this->eatWhiteDefault;
1523 $this->eatWhiteDefault = false;
1524
1525 while ($this->matchString($m, $delim)) {
1526 $content[] = $m[1];
1527
1528 if ($m[2] === '#{') {
1529 $this->count -= strlen($m[2]);
1530
1531 if ($this->interpolation($inter, false)) {
1532 $content[] = $inter;
1533 } else {
1534 $this->count += strlen($m[2]);
1535 $content[] = '#{'; // ignore it
1536 }
1537 } elseif ($m[2] === '\\') {
1538 $content[] = $m[2];
1539
1540 if ($this->literal($delim, false)) {
1541 $content[] = $delim;
1542 }
1543 } else {
1544 $this->count -= strlen($delim);
1545 break; // delim
1546 }
1547 }
1548
1549 $this->eatWhiteDefault = $oldWhite;
1550
1551 if ($this->literal($delim)) {
1552 $out = array('string', $delim, $content);
1553
1554 return true;
1555 }
1556
1557 $this->seek($s);
1558
1559 return false;
1560 }
1561
1562 /**
1563 * Parse keyword or interpolation
1564 *
1565 * @param array $out
1566 *
1567 * @return boolean
1568 */
1569 protected function mixedKeyword(&$out)
1570 {
1571 $s = $this->seek();
1572
1573 $parts = array();
1574
1575 $oldWhite = $this->eatWhiteDefault;
1576 $this->eatWhiteDefault = false;
1577
1578 for (;;) {
1579 if ($this->keyword($key)) {
1580 $parts[] = $key;
1581 continue;
1582 }
1583
1584 if ($this->interpolation($inter)) {
1585 $parts[] = $inter;
1586 continue;
1587 }
1588
1589 break;
1590 }
1591
1592 $this->eatWhiteDefault = $oldWhite;
1593
1594 if (count($parts) === 0) {
1595 return false;
1596 }
1597
1598 if ($this->eatWhiteDefault) {
1599 $this->whitespace();
1600 }
1601
1602 $out = $parts;
1603
1604 return true;
1605 }
1606
1607 /**
1608 * Parse an unbounded string stopped by $end
1609 *
1610 * @param string $end
1611 * @param array $out
1612 * @param string $nestingOpen
1613 *
1614 * @return boolean
1615 */
1616 protected function openString($end, &$out, $nestingOpen = null)
1617 {
1618 $oldWhite = $this->eatWhiteDefault;
1619 $this->eatWhiteDefault = false;
1620
1621 $stop = array('\'', '"', '#{', $end);
1622 $stop = array_map(array($this, 'pregQuote'), $stop);
1623 $stop[] = self::$commentMulti;
1624
1625 $patt = '(.*?)(' . implode('|', $stop) . ')';
1626
1627 $nestingLevel = 0;
1628
1629 $content = array();
1630
1631 while ($this->match($patt, $m, false)) {
1632 if (isset($m[1]) && $m[1] !== '') {
1633 $content[] = $m[1];
1634
1635 if ($nestingOpen) {
1636 $nestingLevel += substr_count($m[1], $nestingOpen);
1637 }
1638 }
1639
1640 $tok = $m[2];
1641
1642 $this->count-= strlen($tok);
1643
1644 if ($tok === $end && ! $nestingLevel--) {
1645 break;
1646 }
1647
1648 if (($tok === '\'' || $tok === '"') && $this->string($str)) {
1649 $content[] = $str;
1650 continue;
1651 }
1652
1653 if ($tok === '#{' && $this->interpolation($inter)) {
1654 $content[] = $inter;
1655 continue;
1656 }
1657
1658 $content[] = $tok;
1659 $this->count+= strlen($tok);
1660 }
1661
1662 $this->eatWhiteDefault = $oldWhite;
1663
1664 if (count($content) === 0) {
1665 return false;
1666 }
1667
1668 // trim the end
1669 if (is_string(end($content))) {
1670 $content[count($content) - 1] = rtrim(end($content));
1671 }
1672
1673 $out = array('string', '', $content);
1674
1675 return true;
1676 }
1677
1678 /**
1679 * Parser interpolation
1680 *
1681 * @param array $out
1682 * @param boolean $lookWhite save information about whitespace before and after
1683 *
1684 * @return boolean
1685 */
1686 protected function interpolation(&$out, $lookWhite = true)
1687 {
1688 $oldWhite = $this->eatWhiteDefault;
1689 $this->eatWhiteDefault = true;
1690
1691 $s = $this->seek();
1692
1693 if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) {
1694 // TODO: don't error if out of bounds
1695
1696 if ($lookWhite) {
1697 $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
1698 $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
1699 } else {
1700 $left = $right = false;
1701 }
1702
1703 $out = array('interpolate', $value, $left, $right);
1704 $this->eatWhiteDefault = $oldWhite;
1705
1706 if ($this->eatWhiteDefault) {
1707 $this->whitespace();
1708 }
1709 return true;
1710 }
1711
1712 $this->seek($s);
1713 $this->eatWhiteDefault = $oldWhite;
1714 return false;
1715 }
1716
1717 /**
1718 * Parse property name (as an array of parts or a string)
1719 *
1720 * @param array $out
1721 *
1722 * @return boolean
1723 */
1724 protected function propertyName(&$out)
1725 {
1726 $s = $this->seek();
1727 $parts = array();
1728
1729 $oldWhite = $this->eatWhiteDefault;
1730 $this->eatWhiteDefault = false;
1731
1732 for (;;) {
1733 if ($this->interpolation($inter)) {
1734 $parts[] = $inter;
1735 } elseif ($this->keyword($text)) {
1736 $parts[] = $text;
1737 } elseif (count($parts) === 0 && $this->match('[:.#]', $m, false)) {
1738 // css hacks
1739 $parts[] = $m[0];
1740 } else {
1741 break;
1742 }
1743 }
1744
1745 $this->eatWhiteDefault = $oldWhite;
1746
1747 if (count($parts) === 0) {
1748 return false;
1749 }
1750
1751 // match comment hack
1752 if (preg_match(
1753 self::$whitePattern,
1754 $this->buffer,
1755 $m,
1756 null,
1757 $this->count
1758 )) {
1759 if (! empty($m[0])) {
1760 $parts[] = $m[0];
1761 $this->count += strlen($m[0]);
1762 }
1763 }
1764
1765 $this->whitespace(); // get any extra whitespace
1766
1767 $out = array('string', '', $parts);
1768
1769 return true;
1770 }
1771
1772 /**
1773 * Parse comma separated selector list
1774 *
1775 * @param array $out
1776 *
1777 * @return boolean
1778 */
1779 protected function selectors(&$out)
1780 {
1781 $s = $this->seek();
1782 $selectors = array();
1783
1784 while ($this->selector($sel)) {
1785 $selectors[] = $sel;
1786
1787 if (! $this->literal(',')) {
1788 break;
1789 }
1790
1791 while ($this->literal(',')) {
1792 ; // ignore extra
1793 }
1794 }
1795
1796 if (count($selectors) === 0) {
1797 $this->seek($s);
1798
1799 return false;
1800 }
1801
1802 $out = $selectors;
1803
1804 return true;
1805 }
1806
1807 /**
1808 * Parse whitespace separated selector list
1809 *
1810 * @param array $out
1811 *
1812 * @return boolean
1813 */
1814 protected function selector(&$out)
1815 {
1816 $selector = array();
1817
1818 for (;;) {
1819 if ($this->match('[>+~]+', $m)) {
1820 $selector[] = array($m[0]);
1821 } elseif ($this->selectorSingle($part)) {
1822 $selector[] = $part;
1823 $this->match('\s+', $m);
1824 } elseif ($this->match('\/[^\/]+\/', $m)) {
1825 $selector[] = array($m[0]);
1826 } else {
1827 break;
1828 }
1829
1830 }
1831
1832 if (count($selector) === 0) {
1833 return false;
1834 }
1835
1836 $out = $selector;
1837 return true;
1838 }
1839
1840 /**
1841 * Parse the parts that make up a selector
1842 *
1843 * {@internal
1844 * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
1845 * }}
1846 *
1847 * @param array $out
1848 *
1849 * @return boolean
1850 */
1851 protected function selectorSingle(&$out)
1852 {
1853 $oldWhite = $this->eatWhiteDefault;
1854 $this->eatWhiteDefault = false;
1855
1856 $parts = array();
1857
1858 if ($this->literal('*', false)) {
1859 $parts[] = '*';
1860 }
1861
1862 for (;;) {
1863 // see if we can stop early
1864 if ($this->match('\s*[{,]', $m)) {
1865 $this->count--;
1866 break;
1867 }
1868
1869 $s = $this->seek();
1870
1871 // self
1872 if ($this->literal('&', false)) {
1873 $parts[] = Compiler::$selfSelector;
1874 continue;
1875 }
1876
1877 if ($this->literal('.', false)) {
1878 $parts[] = '.';
1879 continue;
1880 }
1881
1882 if ($this->literal('|', false)) {
1883 $parts[] = '|';
1884 continue;
1885 }
1886
1887 if ($this->match('\\\\\S', $m)) {
1888 $parts[] = $m[0];
1889 continue;
1890 }
1891
1892 // for keyframes
1893 if ($this->unit($unit)) {
1894 $parts[] = $unit;
1895 continue;
1896 }
1897
1898 if ($this->keyword($name)) {
1899 $parts[] = $name;
1900 continue;
1901 }
1902
1903 if ($this->interpolation($inter)) {
1904 $parts[] = $inter;
1905 continue;
1906 }
1907
1908 if ($this->literal('%', false) && $this->placeholder($placeholder)) {
1909 $parts[] = '%';
1910 $parts[] = $placeholder;
1911 continue;
1912 }
1913
1914 if ($this->literal('#', false)) {
1915 $parts[] = '#';
1916 continue;
1917 }
1918
1919 // a pseudo selector
1920 if ($this->match('::?', $m) && $this->mixedKeyword($nameParts)) {
1921 $parts[] = $m[0];
1922
1923 foreach ($nameParts as $sub) {
1924 $parts[] = $sub;
1925 }
1926
1927 $ss = $this->seek();
1928
1929 if ($this->literal('(') &&
1930 ($this->openString(')', $str, '(') || true ) &&
1931 $this->literal(')')
1932 ) {
1933 $parts[] = '(';
1934
1935 if (! empty($str)) {
1936 $parts[] = $str;
1937 }
1938
1939 $parts[] = ')';
1940 } else {
1941 $this->seek($ss);
1942 }
1943
1944 continue;
1945 }
1946
1947 $this->seek($s);
1948
1949 // attribute selector
1950 // TODO: replace with open string?
1951 if ($this->literal('[', false)) {
1952 $attrParts = array('[');
1953
1954 // keyword, string, operator
1955 for (;;) {
1956 if ($this->literal(']', false)) {
1957 $this->count--;
1958 break; // get out early
1959 }
1960
1961 if ($this->match('\s+', $m)) {
1962 $attrParts[] = ' ';
1963 continue;
1964 }
1965
1966 if ($this->string($str)) {
1967 $attrParts[] = $str;
1968 continue;
1969 }
1970
1971 if ($this->keyword($word)) {
1972 $attrParts[] = $word;
1973 continue;
1974 }
1975
1976 if ($this->interpolation($inter, false)) {
1977 $attrParts[] = $inter;
1978 continue;
1979 }
1980
1981 // operator, handles attr namespace too
1982 if ($this->match('[|-~\$\*\^=]+', $m)) {
1983 $attrParts[] = $m[0];
1984 continue;
1985 }
1986
1987 break;
1988 }
1989
1990 if ($this->literal(']', false)) {
1991 $attrParts[] = ']';
1992
1993 foreach ($attrParts as $part) {
1994 $parts[] = $part;
1995 }
1996
1997 continue;
1998 }
1999
2000 $this->seek($s);
2001 // TODO: should just break here?
2002 }
2003
2004 break;
2005 }
2006
2007 $this->eatWhiteDefault = $oldWhite;
2008
2009 if (count($parts) === 0) {
2010 return false;
2011 }
2012
2013 $out = $parts;
2014
2015 return true;
2016 }
2017
2018 /**
2019 * Parse a variable
2020 *
2021 * @param array $out
2022 *
2023 * @return boolean
2024 */
2025 protected function variable(&$out)
2026 {
2027 $s = $this->seek();
2028
2029 if ($this->literal('$', false) && $this->keyword($name)) {
2030 $out = array('var', $name);
2031
2032 return true;
2033 }
2034
2035 $this->seek($s);
2036
2037 return false;
2038 }
2039
2040 /**
2041 * Parse a keyword
2042 *
2043 * @param string $word
2044 * @param boolean $eatWhitespace
2045 *
2046 * @return boolean
2047 */
2048 protected function keyword(&$word, $eatWhitespace = null)
2049 {
2050 if ($this->match(
2051 '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
2052 $m,
2053 $eatWhitespace
2054 )) {
2055 $word = $m[1];
2056
2057 return true;
2058 }
2059
2060 return false;
2061 }
2062
2063 /**
2064 * Parse a placeholder
2065 *
2066 * @param string $placeholder
2067 *
2068 * @return boolean
2069 */
2070 protected function placeholder(&$placeholder)
2071 {
2072 if ($this->match('([\w\-_]+|#[{][$][\w\-_]+[}])', $m)) {
2073 $placeholder = $m[1];
2074
2075 return true;
2076 }
2077
2078 return false;
2079 }
2080
2081 /**
2082 * Parse a url
2083 *
2084 * @param array $out
2085 *
2086 * @return boolean
2087 */
2088 protected function url(&$out)
2089 {
2090 if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) {
2091 $out = array('string', '', array('url(' . $m[2] . $m[3] . $m[2] . ')'));
2092
2093 return true;
2094 }
2095
2096 return false;
2097 }
2098
2099 /**
2100 * Consume an end of statement delimiter
2101 *
2102 * @return boolean
2103 */
2104 protected function end()
2105 {
2106 if ($this->literal(';')) {
2107 return true;
2108 }
2109
2110 if ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
2111 // if there is end of file or a closing block next then we don't need a ;
2112 return true;
2113 }
2114
2115 return false;
2116 }
2117
2118 /**
2119 * @deprecated
2120 *
2121 * {@internal
2122 * advance counter to next occurrence of $what
2123 * $until - don't include $what in advance
2124 * $allowNewline, if string, will be used as valid char set
2125 * }}
2126 */
2127 protected function to($what, &$out, $until = false, $allowNewline = false)
2128 {
2129 if (is_string($allowNewline)) {
2130 $validChars = $allowNewline;
2131 } else {
2132 $validChars = $allowNewline ? '.' : "[^\n]";
2133 }
2134
2135 if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
2136 return false;
2137 }
2138
2139 if ($until) {
2140 $this->count -= strlen($what); // give back $what
2141 }
2142
2143 $out = $m[1];
2144
2145 return true;
2146 }
2147
2148 /**
2149 * Throw parser error
2150 *
2151 * @param string $msg
2152 * @param integer $count
2153 *
2154 * @throws \Exception
2155 */
2156 public function throwParseError($msg = 'parse error', $count = null)
2157 {
2158 $count = ! isset($count) ? $this->count : $count;
2159
2160 $line = $this->getLineNo($count);
2161
2162 if (! empty($this->sourceName)) {
2163 $loc = "$this->sourceName on line $line";
2164 } else {
2165 $loc = "line: $line";
2166 }
2167
2168 if ($this->peek("(.*?)(\n|$)", $m, $count)) {
2169 throw new \Exception("$msg: failed at `$m[1]` $loc");
2170 }
2171
2172 throw new \Exception("$msg: $loc");
2173 }
2174
2175 /**
2176 * Get source file name
2177 *
2178 * @return string
2179 */
2180 public function getSourceName()
2181 {
2182 return $this->sourceName;
2183 }
2184
2185 /**
2186 * Get source line number (given character position in the buffer)
2187 *
2188 * @param integer $pos
2189 *
2190 * @return integer
2191 */
2192 public function getLineNo($pos)
2193 {
2194 return 1 + substr_count(substr($this->buffer, 0, $pos), "\n");
2195 }
2196
2197 /**
2198 * Match string looking for either ending delim, escape, or string interpolation
2199 *
2200 * {@internal This is a workaround for preg_match's 250K string match limit. }}
2201 *
2202 * @param array $m Matches (passed by reference)
2203 * @param string $delim Delimeter
2204 *
2205 * @return boolean True if match; false otherwise
2206 */
2207 protected function matchString(&$m, $delim)
2208 {
2209 $token = null;
2210
2211 $end = strlen($this->buffer);
2212
2213 // look for either ending delim, escape, or string interpolation
2214 foreach (array('#{', '\\', $delim) as $lookahead) {
2215 $pos = strpos($this->buffer, $lookahead, $this->count);
2216
2217 if ($pos !== false && $pos < $end) {
2218 $end = $pos;
2219 $token = $lookahead;
2220 }
2221 }
2222
2223 if (! isset($token)) {
2224 return false;
2225 }
2226
2227 $match = substr($this->buffer, $this->count, $end - $this->count);
2228 $m = array(
2229 $match . $token,
2230 $match,
2231 $token
2232 );
2233 $this->count = $end + strlen($token);
2234
2235 return true;
2236 }
2237
2238 /**
2239 * Try to match something on head of buffer
2240 *
2241 * @param string $regex
2242 * @param array $out
2243 * @param boolean $eatWhitespace
2244 *
2245 * @return boolean
2246 */
2247 protected function match($regex, &$out, $eatWhitespace = null)
2248 {
2249 if (! isset($eatWhitespace)) {
2250 $eatWhitespace = $this->eatWhiteDefault;
2251 }
2252
2253 $r = '/' . $regex . '/Ais';
2254
2255 if (preg_match($r, $this->buffer, $out, null, $this->count)) {
2256 $this->count += strlen($out[0]);
2257
2258 if ($eatWhitespace) {
2259 $this->whitespace();
2260 }
2261
2262 return true;
2263 }
2264
2265 return false;
2266 }
2267
2268 /**
2269 * Match some whitespace
2270 *
2271 * @return boolean
2272 */
2273 protected function whitespace()
2274 {
2275 $gotWhite = false;
2276
2277 while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
2278 if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
2279 $this->appendComment(array('comment', $m[1]));
2280
2281 $this->commentsSeen[$this->count] = true;
2282 }
2283
2284 $this->count += strlen($m[0]);
2285 $gotWhite = true;
2286 }
2287
2288 return $gotWhite;
2289 }
2290
2291 /**
2292 * Peek input stream
2293 *
2294 * @param string $regex
2295 * @param array $out
2296 * @param integer $from
2297 *
2298 * @return integer
2299 */
2300 protected function peek($regex, &$out, $from = null)
2301 {
2302 if (! isset($from)) {
2303 $from = $this->count;
2304 }
2305
2306 $r = '/' . $regex . '/Ais';
2307 $result = preg_match($r, $this->buffer, $out, null, $from);
2308
2309 return $result;
2310 }
2311
2312 /**
2313 * Seek to position in input stream (or return current position in input stream)
2314 *
2315 * @param integer $where
2316 *
2317 * @return integer
2318 */
2319 protected function seek($where = null)
2320 {
2321 if ($where === null) {
2322 return $this->count;
2323 }
2324
2325 $this->count = $where;
2326
2327 return true;
2328 }
2329
2330 /**
2331 * Quote regular expression
2332 *
2333 * @param string $what
2334 *
2335 * @return string
2336 */
2337 public static function pregQuote($what)
2338 {
2339 return preg_quote($what, '/');
2340 }
2341
2342 /**
2343 * @deprecated
2344 */
2345 protected function show()
2346 {
2347 if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
2348 return $m[1];
2349 }
2350
2351 return '';
2352 }
2353
2354 /**
2355 * Turn list of length 1 into value type
2356 *
2357 * @param array $value
2358 *
2359 * @return array
2360 */
2361 protected function flattenList($value)
2362 {
2363 if ($value[0] === 'list' && count($value[2]) === 1) {
2364 return $this->flattenList($value[2][0]);
2365 }
2366
2367 return $value;
2368 }
2369 }
2370