PluginProbe
GetResponse Forms by Optin Cat / 1.4.1
GetResponse Forms by Optin Cat v1.4.1
1.3.4 1.3.5 1.3.6 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 2.0.0 All 36 releases
getresponse / includes / classes / scssphp / src / Parser.php

Parser.php in GetResponse Forms by Optin Cat 1.4.1, at includes/classes/scssphp/src/Parser.php

1,812 lines 45.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-2014 Leaf Corcoran
6 *
7 * @license http://opensource.org/licenses/gpl-license GPL-3.0
8 * @license http://opensource.org/licenses/MIT MIT
9 *
10 * @link http://leafo.net/scssphp
11 */
12
13 namespace Leafo\ScssPhp;
14
15 use Leafo\ScssPhp\Compiler;
16
17 /**
18 * SCSS parser
19 *
20 * @author Leaf Corcoran <leafot@gmail.com>
21 */
22 class Parser
23 {
24 protected static $precedence = array(
25 'or' => 0,
26 'and' => 1,
27
28 '==' => 2,
29 '!=' => 2,
30 '<=' => 2,
31 '>=' => 2,
32 '=' => 2,
33 '<' => 3,
34 '>' => 2,
35
36 '+' => 3,
37 '-' => 3,
38 '*' => 4,
39 '/' => 4,
40 '%' => 4,
41 );
42
43 protected static $operators = array('+', '-', '*', '/', '%',
44 '==', '!=', '<=', '>=', '<', '>', 'and', 'or');
45
46 protected static $operatorStr;
47 protected static $whitePattern;
48 protected static $commentMulti;
49
50 protected static $commentSingle = '//';
51 protected static $commentMultiLeft = '/*';
52 protected static $commentMultiRight = '*/';
53
54 /**
55 * Constructor
56 *
57 * @param string $sourceName
58 * @param boolean $rootParser
59 */
60 public function __construct($sourceName = null, $rootParser = true)
61 {
62 $this->sourceName = $sourceName;
63 $this->rootParser = $rootParser;
64
65 if (empty(self::$operatorStr)) {
66 self::$operatorStr = $this->makeOperatorStr(self::$operators);
67
68 $commentSingle = $this->pregQuote(self::$commentSingle);
69 $commentMultiLeft = $this->pregQuote(self::$commentMultiLeft);
70 $commentMultiRight = $this->pregQuote(self::$commentMultiRight);
71 self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
72 self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
73 }
74 }
75
76 protected static function makeOperatorStr($operators)
77 {
78 return '('
79 . implode('|', array_map(array('Leafo\ScssPhp\Parser','pregQuote'), $operators))
80 . ')';
81 }
82
83 /**
84 * Parser buffer
85 *
86 * @param string $buffer;
87 *
88 * @return \StdClass
89 */
90 public function parse($buffer)
91 {
92 $this->count = 0;
93 $this->env = null;
94 $this->inParens = false;
95 $this->eatWhiteDefault = true;
96 $this->buffer = $buffer;
97
98 $this->pushBlock(null); // root block
99
100 $this->whitespace();
101 $this->pushBlock(null);
102 $this->popBlock();
103
104 while (false !== $this->parseChunk()) {
105 ;
106 }
107
108 if ($this->count != strlen($this->buffer)) {
109 $this->throwParseError();
110 }
111
112 if (!empty($this->env->parent)) {
113 $this->throwParseError('unclosed block');
114 }
115
116 $this->env->isRoot = true;
117
118 return $this->env;
119 }
120
121 /**
122 * Parse a single chunk off the head of the buffer and append it to the
123 * current parse environment.
124 *
125 * Returns false when the buffer is empty, or when there is an error.
126 *
127 * This function is called repeatedly until the entire document is
128 * parsed.
129 *
130 * This parser is most similar to a recursive descent parser. Single
131 * functions represent discrete grammatical rules for the language, and
132 * they are able to capture the text that represents those rules.
133 *
134 * Consider the function Compiler::keyword(). (All parse functions are
135 * structured the same.)
136 *
137 * The function takes a single reference argument. When calling the
138 * function it will attempt to match a keyword on the head of the buffer.
139 * If it is successful, it will place the keyword in the referenced
140 * argument, advance the position in the buffer, and return true. If it
141 * fails then it won't advance the buffer and it will return false.
142 *
143 * All of these parse functions are powered by Compiler::match(), which behaves
144 * the same way, but takes a literal regular expression. Sometimes it is
145 * more convenient to use match instead of creating a new function.
146 *
147 * Because of the format of the functions, to parse an entire string of
148 * grammatical rules, you can chain them together using &&.
149 *
150 * But, if some of the rules in the chain succeed before one fails, then
151 * the buffer position will be left at an invalid state. In order to
152 * avoid this, Compiler::seek() is used to remember and set buffer positions.
153 *
154 * Before parsing a chain, use $s = $this->seek() to remember the current
155 * position into $s. Then if a chain fails, use $this->seek($s) to
156 * go back where we started.
157 *
158 * @return boolean
159 */
160 protected function parseChunk()
161 {
162 $s = $this->seek();
163
164 // the directives
165 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == '@') {
166 if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) {
167 $media = $this->pushSpecialBlock('media');
168 $media->queryList = $mediaQueryList[2];
169
170 return true;
171 }
172
173 $this->seek($s);
174
175 if ($this->literal('@mixin') &&
176 $this->keyword($mixinName) &&
177 ($this->argumentDef($args) || true) &&
178 $this->literal('{')
179 ) {
180 $mixin = $this->pushSpecialBlock('mixin');
181 $mixin->name = $mixinName;
182 $mixin->args = $args;
183
184 return true;
185 }
186
187 $this->seek($s);
188
189 if ($this->literal('@include') &&
190 $this->keyword($mixinName) &&
191 ($this->literal('(') &&
192 ($this->argValues($argValues) || true) &&
193 $this->literal(')') || true) &&
194 ($this->end() ||
195 $this->literal('{') && $hasBlock = true)
196 ) {
197 $child = array('include',
198 $mixinName, isset($argValues) ? $argValues : null, null);
199
200 if (!empty($hasBlock)) {
201 $include = $this->pushSpecialBlock('include');
202 $include->child = $child;
203 } else {
204 $this->append($child, $s);
205 }
206
207 return true;
208 }
209
210 $this->seek($s);
211
212 if ($this->literal('@import') &&
213 $this->valueList($importPath) &&
214 $this->end()
215 ) {
216 $this->append(array('import', $importPath), $s);
217
218 return true;
219 }
220
221 $this->seek($s);
222
223 if ($this->literal('@extend') &&
224 $this->selectors($selector) &&
225 $this->end()
226 ) {
227 $this->append(array('extend', $selector), $s);
228
229 return true;
230 }
231
232 $this->seek($s);
233
234 if ($this->literal('@function') &&
235 $this->keyword($fnName) &&
236 $this->argumentDef($args) &&
237 $this->literal('{')
238 ) {
239 $func = $this->pushSpecialBlock('function');
240 $func->name = $fnName;
241 $func->args = $args;
242
243 return true;
244 }
245
246 $this->seek($s);
247
248 if ($this->literal('@return') && $this->valueList($retVal) && $this->end()) {
249 $this->append(array('return', $retVal), $s);
250
251 return true;
252 }
253
254 $this->seek($s);
255
256 if ($this->literal('@each') &&
257 $this->variable($varName) &&
258 $this->literal('in') &&
259 $this->valueList($list) &&
260 $this->literal('{')
261 ) {
262 $each = $this->pushSpecialBlock('each');
263 $each->var = $varName[1];
264 $each->list = $list;
265
266 return true;
267 }
268
269 $this->seek($s);
270
271 if ($this->literal('@while') &&
272 $this->expression($cond) &&
273 $this->literal('{')
274 ) {
275 $while = $this->pushSpecialBlock('while');
276 $while->cond = $cond;
277
278 return true;
279 }
280
281 $this->seek($s);
282
283 if ($this->literal('@for') &&
284 $this->variable($varName) &&
285 $this->literal('from') &&
286 $this->expression($start) &&
287 ($this->literal('through') ||
288 ($forUntil = true && $this->literal('to'))) &&
289 $this->expression($end) &&
290 $this->literal('{')
291 ) {
292 $for = $this->pushSpecialBlock('for');
293 $for->var = $varName[1];
294 $for->start = $start;
295 $for->end = $end;
296 $for->until = isset($forUntil);
297
298 return true;
299 }
300
301 $this->seek($s);
302
303 if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) {
304 $if = $this->pushSpecialBlock('if');
305 $if->cond = $cond;
306 $if->cases = array();
307
308 return true;
309 }
310
311 $this->seek($s);
312
313 if (($this->literal('@debug') || $this->literal('@warn')) &&
314 $this->valueList($value) &&
315 $this->end()) {
316 $this->append(array('debug', $value, $s), $s);
317
318 return true;
319 }
320
321 $this->seek($s);
322
323 if ($this->literal('@content') && $this->end()) {
324 $this->append(array('mixin_content'), $s);
325
326 return true;
327 }
328
329 $this->seek($s);
330
331 $last = $this->last();
332 if (isset($last) && $last[0] == 'if') {
333 list(, $if) = $last;
334
335 if ($this->literal('@else')) {
336 if ($this->literal('{')) {
337 $else = $this->pushSpecialBlock('else');
338 } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) {
339 $else = $this->pushSpecialBlock('elseif');
340 $else->cond = $cond;
341 }
342
343 if (isset($else)) {
344 $else->dontAppend = true;
345 $if->cases[] = $else;
346
347 return true;
348 }
349 }
350
351 $this->seek($s);
352 }
353
354 if ($this->literal('@charset') &&
355 $this->valueList($charset) && $this->end()
356 ) {
357 $this->append(array('charset', $charset), $s);
358
359 return true;
360 }
361
362 $this->seek($s);
363
364 // doesn't match built in directive, do generic one
365 if ($this->literal('@', false) && $this->keyword($dirName) &&
366 ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
367 $this->literal('{')
368 ) {
369 $directive = $this->pushSpecialBlock('directive');
370 $directive->name = $dirName;
371
372 if (isset($dirValue)) {
373 $directive->value = $dirValue;
374 }
375
376 return true;
377 }
378
379 $this->seek($s);
380
381 return false;
382 }
383
384 // property shortcut
385 // captures most properties before having to parse a selector
386 if ($this->keyword($name, false) &&
387 $this->literal(': ') &&
388 $this->valueList($value) &&
389 $this->end()
390 ) {
391 $name = array('string', '', array($name));
392 $this->append(array('assign', $name, $value), $s);
393
394 return true;
395 }
396
397 $this->seek($s);
398
399 // variable assigns
400 if ($this->variable($name) &&
401 $this->literal(':') &&
402 $this->valueList($value) && $this->end()
403 ) {
404 // check for !default
405 $defaultVar = $value[0] == 'list' && $this->stripDefault($value);
406 $this->append(array('assign', $name, $value, $defaultVar), $s);
407
408 return true;
409 }
410
411 $this->seek($s);
412
413 // misc
414 if ($this->literal('-->')) {
415 return true;
416 }
417
418 // opening css block
419 if ($this->selectors($selectors) && $this->literal('{')) {
420 $b = $this->pushBlock($selectors);
421
422 return true;
423 }
424
425 $this->seek($s);
426
427 // property assign, or nested assign
428 if ($this->propertyName($name) && $this->literal(':')) {
429 $foundSomething = false;
430 if ($this->valueList($value)) {
431 $this->append(array('assign', $name, $value), $s);
432 $foundSomething = true;
433 }
434
435 if ($this->literal('{')) {
436 $propBlock = $this->pushSpecialBlock('nestedprop');
437 $propBlock->prefix = $name;
438 $foundSomething = true;
439 } elseif ($foundSomething) {
440 $foundSomething = $this->end();
441 }
442
443 if ($foundSomething) {
444 return true;
445 }
446 }
447
448 $this->seek($s);
449
450 // closing a block
451 if ($this->literal('}')) {
452 $block = $this->popBlock();
453
454 if (isset($block->type) && $block->type == 'include') {
455 $include = $block->child;
456 unset($block->child);
457 $include[3] = $block;
458 $this->append($include, $s);
459 } elseif (empty($block->dontAppend)) {
460 $type = isset($block->type) ? $block->type : 'block';
461 $this->append(array($type, $block), $s);
462 }
463
464 return true;
465 }
466
467 // extra stuff
468 if ($this->literal(';') ||
469 $this->literal('<!--')
470 ) {
471 return true;
472 }
473
474 return false;
475 }
476
477 protected function stripDefault(&$value)
478 {
479 $def = end($value[2]);
480
481 if ($def[0] == 'keyword' && $def[1] == '!default') {
482 array_pop($value[2]);
483 $value = $this->flattenList($value);
484
485 return true;
486 }
487
488 if ($def[0] == 'list') {
489 return $this->stripDefault($value[2][count($value[2]) - 1]);
490 }
491
492 return false;
493 }
494
495 protected function literal($what, $eatWhitespace = null)
496 {
497 if (!isset($eatWhitespace)) {
498 $eatWhitespace = $this->eatWhiteDefault;
499 }
500
501 // shortcut on single letter
502 if (!isset($what[1]) && isset($this->buffer[$this->count])) {
503 if ($this->buffer[$this->count] == $what) {
504 if (!$eatWhitespace) {
505 $this->count++;
506
507 return true;
508 }
509 // goes below...
510 } else {
511 return false;
512 }
513 }
514
515 return $this->match($this->pregQuote($what), $m, $eatWhitespace);
516 }
517
518 // tree builders
519
520 protected function pushBlock($selectors)
521 {
522 $b = new \stdClass;
523 $b->parent = $this->env; // not sure if we need this yet
524
525 $b->selectors = $selectors;
526 $b->comments = array();
527
528 if (!$this->env) {
529 $b->children = array();
530 } elseif (empty($this->env->children)) {
531 $this->env->children = $this->env->comments;
532 $b->children = array();
533 $this->env->comments = array();
534 } else {
535 $b->children = $this->env->comments;
536 $this->env->comments = array();
537 }
538
539 $this->env = $b;
540
541 return $b;
542 }
543
544 protected function pushSpecialBlock($type)
545 {
546 $block = $this->pushBlock(null);
547 $block->type = $type;
548
549 return $block;
550 }
551
552 protected function popBlock()
553 {
554 $block = $this->env;
555
556 if (empty($block->parent)) {
557 $this->throwParseError('unexpected }');
558 }
559
560 $this->env = $block->parent;
561 unset($block->parent);
562
563 $comments = $block->comments;
564 if (count($comments)) {
565 $this->env->comments = $comments;
566 unset($block->comments);
567 }
568
569 return $block;
570 }
571
572 protected function appendComment($comment)
573 {
574 $comment[1] = substr(preg_replace(array('/^\s+/m', '/^(.)/m'), array('', ' \1'), $comment[1]), 1);
575
576 $this->env->comments[] = $comment;
577 }
578
579 protected function append($statement, $pos = null)
580 {
581 if ($pos !== null) {
582 $statement[-1] = $pos;
583
584 if (!$this->rootParser) {
585 $statement[-2] = $this;
586 }
587 }
588
589 $this->env->children[] = $statement;
590
591 $comments = $this->env->comments;
592 if (count($comments)) {
593 $this->env->children = array_merge($this->env->children, $comments);
594 $this->env->comments = array();
595 }
596 }
597
598 // last child that was appended
599 protected function last()
600 {
601 $i = count($this->env->children) - 1;
602
603 if (isset($this->env->children[$i])) {
604 return $this->env->children[$i];
605 }
606 }
607
608 // high level parsers (they return parts of ast)
609
610 protected function mediaQueryList(&$out)
611 {
612 return $this->genericList($out, 'mediaQuery', ',', false);
613 }
614
615 protected function mediaQuery(&$out)
616 {
617 $s = $this->seek();
618
619 $expressions = null;
620 $parts = array();
621
622 if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) &&
623 $this->mixedKeyword($mediaType)
624 ) {
625 $prop = array('mediaType');
626
627 if (isset($only)) {
628 $prop[] = array('keyword', 'only');
629 }
630
631 if (isset($not)) {
632 $prop[] = array('keyword', 'not');
633 }
634
635 $media = array('list', '', array());
636
637 foreach ((array)$mediaType as $type) {
638 if (is_array($type)) {
639 $media[2][] = $type;
640 } else {
641 $media[2][] = array('keyword', $type);
642 }
643 }
644
645 $prop[] = $media;
646 $parts[] = $prop;
647 }
648
649 if (empty($parts) || $this->literal('and')) {
650 $this->genericList($expressions, 'mediaExpression', 'and', false);
651
652 if (is_array($expressions)) {
653 $parts = array_merge($parts, $expressions[2]);
654 }
655 }
656
657 $out = $parts;
658
659 return true;
660 }
661
662 protected function mediaExpression(&$out)
663 {
664 $s = $this->seek();
665 $value = null;
666
667 if ($this->literal('(') &&
668 $this->expression($feature) &&
669 ($this->literal(':') && $this->expression($value) || true) &&
670 $this->literal(')')
671 ) {
672 $out = array('mediaExp', $feature);
673
674 if ($value) {
675 $out[] = $value;
676 }
677
678 return true;
679 }
680
681 $this->seek($s);
682
683 return false;
684 }
685
686 protected function argValues(&$out)
687 {
688 if ($this->genericList($list, 'argValue', ',', false)) {
689 $out = $list[2];
690
691 return true;
692 }
693
694 return false;
695 }
696
697 protected function argValue(&$out)
698 {
699 $s = $this->seek();
700
701 $keyword = null;
702 if (!$this->variable($keyword) || !$this->literal(':')) {
703 $this->seek($s);
704 $keyword = null;
705 }
706
707 if ($this->genericList($value, 'expression')) {
708 $out = array($keyword, $value, false);
709 $s = $this->seek();
710
711 if ($this->literal('...')) {
712 $out[2] = true;
713 } else {
714 $this->seek($s);
715 }
716
717 return true;
718 }
719
720 return false;
721 }
722
723 /**
724 * Parse list
725 *
726 * @param string $out
727 *
728 * @return boolean
729 */
730 public function valueList(&$out)
731 {
732 return $this->genericList($out, 'spaceList', ',');
733 }
734
735 protected function spaceList(&$out)
736 {
737 return $this->genericList($out, 'expression');
738 }
739
740 protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
741 {
742 $s = $this->seek();
743 $items = array();
744
745 while ($this->$parseItem($value)) {
746 $items[] = $value;
747
748 if ($delim) {
749 if (!$this->literal($delim)) {
750 break;
751 }
752 }
753 }
754
755 if (count($items) == 0) {
756 $this->seek($s);
757
758 return false;
759 }
760
761 if ($flatten && count($items) == 1) {
762 $out = $items[0];
763 } else {
764 $out = array('list', $delim, $items);
765 }
766
767 return true;
768 }
769
770 protected function expression(&$out)
771 {
772 $s = $this->seek();
773
774 if ($this->literal('(')) {
775 if ($this->literal(')')) {
776 $out = array('list', '', array());
777
778 return true;
779 }
780
781 if ($this->valueList($out) && $this->literal(')') && $out[0] == 'list') {
782 return true;
783 }
784
785 $this->seek($s);
786 }
787
788 if ($this->value($lhs)) {
789 $out = $this->expHelper($lhs, 0);
790
791 return true;
792 }
793
794 return false;
795 }
796
797 protected function expHelper($lhs, $minP)
798 {
799 $opstr = self::$operatorStr;
800
801 $ss = $this->seek();
802 $whiteBefore = isset($this->buffer[$this->count - 1]) &&
803 ctype_space($this->buffer[$this->count - 1]);
804
805 while ($this->match($opstr, $m) && self::$precedence[$m[1]] >= $minP) {
806 $whiteAfter = isset($this->buffer[$this->count - 1]) &&
807 ctype_space($this->buffer[$this->count - 1]);
808
809 $op = $m[1];
810
811 // don't turn negative numbers into expressions
812 if ($op == '-' && $whiteBefore) {
813 if (!$whiteAfter) {
814 break;
815 }
816 }
817
818 if (!$this->value($rhs)) {
819 break;
820 }
821
822 // peek and see if rhs belongs to next operator
823 if ($this->peek($opstr, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
824 $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
825 }
826
827 $lhs = array('exp', $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter);
828 $ss = $this->seek();
829 $whiteBefore = isset($this->buffer[$this->count - 1]) &&
830 ctype_space($this->buffer[$this->count - 1]);
831 }
832
833 $this->seek($ss);
834
835 return $lhs;
836 }
837
838 protected function value(&$out)
839 {
840 $s = $this->seek();
841
842 if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) {
843 $out = array('unary', 'not', $inner, $this->inParens);
844
845 return true;
846 }
847
848 $this->seek($s);
849
850 if ($this->literal('+') && $this->value($inner)) {
851 $out = array('unary', '+', $inner, $this->inParens);
852
853 return true;
854 }
855
856 $this->seek($s);
857
858 // negation
859 if ($this->literal('-', false) &&
860 ($this->variable($inner) ||
861 $this->unit($inner) ||
862 $this->parenValue($inner))
863 ) {
864 $out = array('unary', '-', $inner, $this->inParens);
865
866 return true;
867 }
868
869 $this->seek($s);
870
871 if ($this->parenValue($out) ||
872 $this->interpolation($out) ||
873 $this->variable($out) ||
874 $this->color($out) ||
875 $this->unit($out) ||
876 $this->string($out) ||
877 $this->func($out) ||
878 $this->progid($out)
879 ) {
880 return true;
881 }
882
883 if ($this->keyword($keyword)) {
884 if ($keyword == 'null') {
885 $out = array('null');
886 } else {
887 $out = array('keyword', $keyword);
888 }
889
890 return true;
891 }
892
893 return false;
894 }
895
896 // value wrappen in parentheses
897 protected function parenValue(&$out)
898 {
899 $s = $this->seek();
900
901 $inParens = $this->inParens;
902
903 if ($this->literal('(') &&
904 ($this->inParens = true) && $this->expression($exp) &&
905 $this->literal(')')
906 ) {
907 $out = $exp;
908 $this->inParens = $inParens;
909
910 return true;
911 }
912
913 $this->inParens = $inParens;
914 $this->seek($s);
915
916 return false;
917 }
918
919 protected function progid(&$out)
920 {
921 $s = $this->seek();
922
923 if ($this->literal('progid:', false) &&
924 $this->openString('(', $fn) &&
925 $this->literal('(')
926 ) {
927 $this->openString(')', $args, '(');
928 if ($this->literal(')')) {
929 $out = array('string', '', array(
930 'progid:', $fn, '(', $args, ')'
931 ));
932
933 return true;
934 }
935 }
936
937 $this->seek($s);
938
939 return false;
940 }
941
942 protected function func(&$func)
943 {
944 $s = $this->seek();
945
946 if ($this->keyword($name, false) &&
947 $this->literal('(')
948 ) {
949 if ($name == 'alpha' && $this->argumentList($args)) {
950 $func = array('function', $name, array('string', '', $args));
951
952 return true;
953 }
954
955 if ($name != 'expression' && !preg_match('/^(-[a-z]+-)?calc$/', $name)) {
956 $ss = $this->seek();
957
958 if ($this->argValues($args) && $this->literal(')')) {
959 $func = array('fncall', $name, $args);
960
961 return true;
962 }
963
964 $this->seek($ss);
965 }
966
967 if (($this->openString(')', $str, '(') || true ) &&
968 $this->literal(')')
969 ) {
970 $args = array();
971
972 if (!empty($str)) {
973 $args[] = array(null, array('string', '', array($str)));
974 }
975
976 $func = array('fncall', $name, $args);
977
978 return true;
979 }
980 }
981
982 $this->seek($s);
983
984 return false;
985 }
986
987 protected function argumentList(&$out)
988 {
989 $s = $this->seek();
990 $this->literal('(');
991
992 $args = array();
993
994 while ($this->keyword($var)) {
995 $ss = $this->seek();
996
997 if ($this->literal('=') && $this->expression($exp)) {
998 $args[] = array('string', '', array($var.'='));
999 $arg = $exp;
1000 } else {
1001 break;
1002 }
1003
1004 $args[] = $arg;
1005
1006 if (!$this->literal(',')) {
1007 break;
1008 }
1009
1010 $args[] = array('string', '', array(', '));
1011 }
1012
1013 if (!$this->literal(')') || !count($args)) {
1014 $this->seek($s);
1015
1016 return false;
1017 }
1018
1019 $out = $args;
1020
1021 return true;
1022 }
1023
1024 protected function argumentDef(&$out)
1025 {
1026 $s = $this->seek();
1027 $this->literal('(');
1028
1029 $args = array();
1030
1031 while ($this->variable($var)) {
1032 $arg = array($var[1], null, false);
1033
1034 $ss = $this->seek();
1035
1036 if ($this->literal(':') && $this->genericList($defaultVal, 'expression')) {
1037 $arg[1] = $defaultVal;
1038 } else {
1039 $this->seek($ss);
1040 }
1041
1042 $ss = $this->seek();
1043
1044 if ($this->literal('...')) {
1045 $sss = $this->seek();
1046
1047 if (!$this->literal(')')) {
1048 $this->throwParseError('... has to be after the final argument');
1049 }
1050
1051 $arg[2] = true;
1052 $this->seek($sss);
1053 } else {
1054 $this->seek($ss);
1055 }
1056
1057 $args[] = $arg;
1058
1059 if (!$this->literal(',')) {
1060 break;
1061 }
1062 }
1063
1064 if (!$this->literal(')')) {
1065 $this->seek($s);
1066
1067 return false;
1068 }
1069
1070 $out = $args;
1071
1072 return true;
1073 }
1074
1075 protected function color(&$out)
1076 {
1077 $color = array('color');
1078
1079 if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
1080 if (isset($m[3])) {
1081 $num = $m[3];
1082 $width = 16;
1083 } else {
1084 $num = $m[2];
1085 $width = 256;
1086 }
1087
1088 $num = hexdec($num);
1089
1090 foreach (array(3,2,1) as $i) {
1091 $t = $num % $width;
1092 $num /= $width;
1093
1094 $color[$i] = $t * (256/$width) + $t * floor(16/$width);
1095 }
1096
1097 $out = $color;
1098
1099 return true;
1100 }
1101
1102 return false;
1103 }
1104
1105 protected function unit(&$unit)
1106 {
1107 if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
1108 $unit = array('number', $m[1], empty($m[3]) ? '' : $m[3]);
1109
1110 return true;
1111 }
1112
1113 return false;
1114 }
1115
1116 protected function string(&$out)
1117 {
1118 $s = $this->seek();
1119
1120 if ($this->literal('"', false)) {
1121 $delim = '"';
1122 } elseif ($this->literal('\'', false)) {
1123 $delim = '\'';
1124 } else {
1125 return false;
1126 }
1127
1128 $content = array();
1129 $oldWhite = $this->eatWhiteDefault;
1130 $this->eatWhiteDefault = false;
1131
1132 while ($this->matchString($m, $delim)) {
1133 $content[] = $m[1];
1134
1135 if ($m[2] == '#{') {
1136 $this->count -= strlen($m[2]);
1137
1138 if ($this->interpolation($inter, false)) {
1139 $content[] = $inter;
1140 } else {
1141 $this->count += strlen($m[2]);
1142 $content[] = '#{'; // ignore it
1143 }
1144 } elseif ($m[2] == '\\') {
1145 $content[] = $m[2];
1146
1147 if ($this->literal($delim, false)) {
1148 $content[] = $delim;
1149 }
1150 } else {
1151 $this->count -= strlen($delim);
1152 break; // delim
1153 }
1154 }
1155
1156 $this->eatWhiteDefault = $oldWhite;
1157
1158 if ($this->literal($delim)) {
1159 $out = array('string', $delim, $content);
1160
1161 return true;
1162 }
1163
1164 $this->seek($s);
1165
1166 return false;
1167 }
1168
1169 protected function mixedKeyword(&$out)
1170 {
1171 $s = $this->seek();
1172
1173 $parts = array();
1174
1175 $oldWhite = $this->eatWhiteDefault;
1176 $this->eatWhiteDefault = false;
1177
1178 while (true) {
1179 if ($this->keyword($key)) {
1180 $parts[] = $key;
1181 continue;
1182 }
1183
1184 if ($this->interpolation($inter)) {
1185 $parts[] = $inter;
1186 continue;
1187 }
1188
1189 break;
1190 }
1191
1192 $this->eatWhiteDefault = $oldWhite;
1193
1194 if (count($parts) == 0) {
1195 return false;
1196 }
1197
1198 if ($this->eatWhiteDefault) {
1199 $this->whitespace();
1200 }
1201
1202 $out = $parts;
1203
1204 return true;
1205 }
1206
1207 // an unbounded string stopped by $end
1208 protected function openString($end, &$out, $nestingOpen = null)
1209 {
1210 $oldWhite = $this->eatWhiteDefault;
1211 $this->eatWhiteDefault = false;
1212
1213 $stop = array('\'', '"', '#{', $end);
1214 $stop = array_map(array($this, 'pregQuote'), $stop);
1215 $stop[] = self::$commentMulti;
1216
1217 $patt = '(.*?)('.implode('|', $stop).')';
1218
1219 $nestingLevel = 0;
1220
1221 $content = array();
1222 while ($this->match($patt, $m, false)) {
1223 if (isset($m[1]) && $m[1] !== '') {
1224 $content[] = $m[1];
1225 if ($nestingOpen) {
1226 $nestingLevel += substr_count($m[1], $nestingOpen);
1227 }
1228 }
1229
1230 $tok = $m[2];
1231
1232 $this->count-= strlen($tok);
1233 if ($tok == $end) {
1234 if ($nestingLevel == 0) {
1235 break;
1236 } else {
1237 $nestingLevel--;
1238 }
1239 }
1240
1241 if (($tok == '\'' || $tok == '"') && $this->string($str)) {
1242 $content[] = $str;
1243 continue;
1244 }
1245
1246 if ($tok == '#{' && $this->interpolation($inter)) {
1247 $content[] = $inter;
1248 continue;
1249 }
1250
1251 $content[] = $tok;
1252 $this->count+= strlen($tok);
1253 }
1254
1255 $this->eatWhiteDefault = $oldWhite;
1256
1257 if (count($content) == 0) {
1258 return false;
1259 }
1260
1261 // trim the end
1262 if (is_string(end($content))) {
1263 $content[count($content) - 1] = rtrim(end($content));
1264 }
1265
1266 $out = array('string', '', $content);
1267
1268 return true;
1269 }
1270
1271 // $lookWhite: save information about whitespace before and after
1272 protected function interpolation(&$out, $lookWhite = true)
1273 {
1274 $oldWhite = $this->eatWhiteDefault;
1275 $this->eatWhiteDefault = true;
1276
1277 $s = $this->seek();
1278 if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) {
1279
1280 // TODO: don't error if out of bounds
1281
1282 if ($lookWhite) {
1283 $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
1284 $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
1285 } else {
1286 $left = $right = false;
1287 }
1288
1289 $out = array('interpolate', $value, $left, $right);
1290 $this->eatWhiteDefault = $oldWhite;
1291 if ($this->eatWhiteDefault) {
1292 $this->whitespace();
1293 }
1294 return true;
1295 }
1296
1297 $this->seek($s);
1298 $this->eatWhiteDefault = $oldWhite;
1299 return false;
1300 }
1301
1302 // low level parsers
1303
1304 // returns an array of parts or a string
1305 protected function propertyName(&$out)
1306 {
1307 $s = $this->seek();
1308 $parts = array();
1309
1310 $oldWhite = $this->eatWhiteDefault;
1311 $this->eatWhiteDefault = false;
1312
1313 while (true) {
1314 if ($this->interpolation($inter)) {
1315 $parts[] = $inter;
1316 } elseif ($this->keyword($text)) {
1317 $parts[] = $text;
1318 } elseif (count($parts) == 0 && $this->match('[:.#]', $m, false)) {
1319 // css hacks
1320 $parts[] = $m[0];
1321 } else {
1322 break;
1323 }
1324 }
1325
1326 $this->eatWhiteDefault = $oldWhite;
1327 if (count($parts) == 0) {
1328 return false;
1329 }
1330
1331 // match comment hack
1332 if (preg_match(
1333 self::$whitePattern,
1334 $this->buffer,
1335 $m,
1336 null,
1337 $this->count
1338 )) {
1339 if (!empty($m[0])) {
1340 $parts[] = $m[0];
1341 $this->count += strlen($m[0]);
1342 }
1343 }
1344
1345 $this->whitespace(); // get any extra whitespace
1346
1347 $out = array('string', '', $parts);
1348
1349 return true;
1350 }
1351
1352 // comma separated list of selectors
1353 protected function selectors(&$out)
1354 {
1355 $s = $this->seek();
1356 $selectors = array();
1357
1358 while ($this->selector($sel)) {
1359 $selectors[] = $sel;
1360
1361 if (!$this->literal(',')) {
1362 break;
1363 }
1364
1365 while ($this->literal(',')) {
1366 ; // ignore extra
1367 }
1368 }
1369
1370 if (count($selectors) == 0) {
1371 $this->seek($s);
1372
1373 return false;
1374 }
1375
1376 $out = $selectors;
1377
1378 return true;
1379 }
1380
1381 // whitespace separated list of selectorSingle
1382 protected function selector(&$out)
1383 {
1384 $selector = array();
1385
1386 while (true) {
1387 if ($this->match('[>+~]+', $m)) {
1388 $selector[] = array($m[0]);
1389 } elseif ($this->selectorSingle($part)) {
1390 $selector[] = $part;
1391 $this->match('\s+', $m);
1392 } elseif ($this->match('\/[^\/]+\/', $m)) {
1393 $selector[] = array($m[0]);
1394 } else {
1395 break;
1396 }
1397
1398 }
1399
1400 if (count($selector) == 0) {
1401 return false;
1402 }
1403
1404 $out = $selector;
1405 return true;
1406 }
1407
1408 // the parts that make up
1409 // div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
1410 protected function selectorSingle(&$out)
1411 {
1412 $oldWhite = $this->eatWhiteDefault;
1413 $this->eatWhiteDefault = false;
1414
1415 $parts = array();
1416
1417 if ($this->literal('*', false)) {
1418 $parts[] = '*';
1419 }
1420
1421 while (true) {
1422 // see if we can stop early
1423 if ($this->match('\s*[{,]', $m)) {
1424 $this->count--;
1425 break;
1426 }
1427
1428 $s = $this->seek();
1429 // self
1430 if ($this->literal('&', false)) {
1431 $parts[] = Compiler::$selfSelector;
1432 continue;
1433 }
1434
1435 if ($this->literal('.', false)) {
1436 $parts[] = '.';
1437 continue;
1438 }
1439
1440 if ($this->literal('|', false)) {
1441 $parts[] = '|';
1442 continue;
1443 }
1444
1445 if ($this->match('\\\\\S', $m)) {
1446 $parts[] = $m[0];
1447 continue;
1448 }
1449
1450 // for keyframes
1451 if ($this->unit($unit)) {
1452 $parts[] = $unit;
1453 continue;
1454 }
1455
1456 if ($this->keyword($name)) {
1457 $parts[] = $name;
1458 continue;
1459 }
1460
1461 if ($this->interpolation($inter)) {
1462 $parts[] = $inter;
1463 continue;
1464 }
1465
1466 if ($this->literal('%', false) && $this->placeholder($placeholder)) {
1467 $parts[] = '%';
1468 $parts[] = $placeholder;
1469 continue;
1470 }
1471
1472 if ($this->literal('#', false)) {
1473 $parts[] = '#';
1474 continue;
1475 }
1476
1477 // a pseudo selector
1478 if ($this->match('::?', $m) && $this->mixedKeyword($nameParts)) {
1479 $parts[] = $m[0];
1480 foreach ($nameParts as $sub) {
1481 $parts[] = $sub;
1482 }
1483
1484 $ss = $this->seek();
1485
1486 if ($this->literal('(') &&
1487 ($this->openString(')', $str, '(') || true ) &&
1488 $this->literal(')')
1489 ) {
1490 $parts[] = '(';
1491
1492 if (!empty($str)) {
1493 $parts[] = $str;
1494 }
1495
1496 $parts[] = ')';
1497 } else {
1498 $this->seek($ss);
1499 }
1500
1501 continue;
1502 } else {
1503 $this->seek($s);
1504 }
1505
1506 // attribute selector
1507 // TODO: replace with open string?
1508 if ($this->literal('[', false)) {
1509 $attrParts = array('[');
1510 // keyword, string, operator
1511 while (true) {
1512 if ($this->literal(']', false)) {
1513 $this->count--;
1514 break; // get out early
1515 }
1516
1517 if ($this->match('\s+', $m)) {
1518 $attrParts[] = ' ';
1519 continue;
1520 }
1521 if ($this->string($str)) {
1522 $attrParts[] = $str;
1523 continue;
1524 }
1525
1526 if ($this->keyword($word)) {
1527 $attrParts[] = $word;
1528 continue;
1529 }
1530
1531 if ($this->interpolation($inter, false)) {
1532 $attrParts[] = $inter;
1533 continue;
1534 }
1535
1536 // operator, handles attr namespace too
1537 if ($this->match('[|-~\$\*\^=]+', $m)) {
1538 $attrParts[] = $m[0];
1539 continue;
1540 }
1541
1542 break;
1543 }
1544
1545 if ($this->literal(']', false)) {
1546 $attrParts[] = ']';
1547
1548 foreach ($attrParts as $part) {
1549 $parts[] = $part;
1550 }
1551
1552 continue;
1553 }
1554
1555 $this->seek($s);
1556 // should just break here?
1557 }
1558
1559 break;
1560 }
1561
1562 $this->eatWhiteDefault = $oldWhite;
1563
1564 if (count($parts) == 0) {
1565 return false;
1566 }
1567
1568 $out = $parts;
1569
1570 return true;
1571 }
1572
1573 protected function variable(&$out)
1574 {
1575 $s = $this->seek();
1576
1577 if ($this->literal('$', false) && $this->keyword($name)) {
1578 $out = array('var', $name);
1579
1580 return true;
1581 }
1582
1583 $this->seek($s);
1584
1585 return false;
1586 }
1587
1588 protected function keyword(&$word, $eatWhitespace = null)
1589 {
1590 if ($this->match(
1591 '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
1592 $m,
1593 $eatWhitespace
1594 )) {
1595 $word = $m[1];
1596
1597 return true;
1598 }
1599
1600 return false;
1601 }
1602
1603 protected function placeholder(&$placeholder)
1604 {
1605 if ($this->match('([\w\-_]+)', $m)) {
1606 $placeholder = $m[1];
1607
1608 return true;
1609 }
1610
1611 return false;
1612 }
1613
1614 // consume an end of statement delimiter
1615 protected function end()
1616 {
1617 if ($this->literal(';')) {
1618 return true;
1619 }
1620
1621 if ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
1622 // if there is end of file or a closing block next then we don't need a ;
1623 return true;
1624 }
1625
1626 return false;
1627 }
1628
1629 // advance counter to next occurrence of $what
1630 // $until - don't include $what in advance
1631 // $allowNewline, if string, will be used as valid char set
1632 protected function to($what, &$out, $until = false, $allowNewline = false)
1633 {
1634 if (is_string($allowNewline)) {
1635 $validChars = $allowNewline;
1636 } else {
1637 $validChars = $allowNewline ? '.' : "[^\n]";
1638 }
1639
1640 if (!$this->match('('.$validChars.'*?)'.$this->pregQuote($what), $m, !$until)) {
1641 return false;
1642 }
1643
1644 if ($until) {
1645 $this->count -= strlen($what); // give back $what
1646 }
1647
1648 $out = $m[1];
1649
1650 return true;
1651 }
1652
1653 public function throwParseError($msg = 'parse error', $count = null)
1654 {
1655 $count = !isset($count) ? $this->count : $count;
1656
1657 $line = $this->getLineNo($count);
1658
1659 if (!empty($this->sourceName)) {
1660 $loc = "$this->sourceName on line $line";
1661 } else {
1662 $loc = "line: $line";
1663 }
1664
1665 if ($this->peek("(.*?)(\n|$)", $m, $count)) {
1666 throw new \Exception("$msg: failed at `$m[1]` $loc");
1667 }
1668
1669 throw new \Exception("$msg: $loc");
1670 }
1671
1672 public function getLineNo($pos)
1673 {
1674 return 1 + substr_count(substr($this->buffer, 0, $pos), "\n");
1675 }
1676
1677 /**
1678 * Match string looking for either ending delim, escape, or string interpolation
1679 *
1680 * {@internal This is a workaround for preg_match's 250K string match limit. }}
1681 *
1682 * @param array $m Matches (passed by reference)
1683 * @param string $delim Delimeter
1684 *
1685 * @return boolean True if match; false otherwise
1686 */
1687 protected function matchString(&$m, $delim)
1688 {
1689 $token = null;
1690
1691 $end = strpos($this->buffer, "\n", $this->count);
1692
1693 if ($end === false ||
1694 $this->buffer[$end - 1] == '\\' ||
1695 $this->buffer[$end - 2] == '\\' && $this->buffer[$end - 1] == "\r"
1696 ) {
1697 $end = strlen($this->buffer);
1698 }
1699
1700 // look for either ending delim, escape, or string interpolation
1701 foreach (array('#{', '\\', $delim) as $lookahead) {
1702 $pos = strpos($this->buffer, $lookahead, $this->count);
1703
1704 if ($pos !== false && $pos < $end) {
1705 $end = $pos;
1706 $token = $lookahead;
1707 }
1708 }
1709
1710 if (!isset($token)) {
1711 return false;
1712 }
1713
1714 $match = substr($this->buffer, $this->count, $end - $this->count);
1715 $m = array(
1716 $match . $token,
1717 $match,
1718 $token
1719 );
1720 $this->count = $end + strlen($token);
1721
1722 return true;
1723 }
1724
1725 // try to match something on head of buffer
1726 protected function match($regex, &$out, $eatWhitespace = null)
1727 {
1728 if (!isset($eatWhitespace)) {
1729 $eatWhitespace = $this->eatWhiteDefault;
1730 }
1731
1732 $r = '/'.$regex.'/Ais';
1733
1734 if (preg_match($r, $this->buffer, $out, null, $this->count)) {
1735 $this->count += strlen($out[0]);
1736
1737 if ($eatWhitespace) {
1738 $this->whitespace();
1739 }
1740
1741 return true;
1742 }
1743
1744 return false;
1745 }
1746
1747 // match some whitespace
1748 protected function whitespace()
1749 {
1750 $gotWhite = false;
1751
1752 while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
1753 if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
1754 $this->appendComment(array('comment', $m[1]));
1755 $this->commentsSeen[$this->count] = true;
1756 }
1757
1758 $this->count += strlen($m[0]);
1759 $gotWhite = true;
1760 }
1761
1762 return $gotWhite;
1763 }
1764
1765 protected function peek($regex, &$out, $from = null)
1766 {
1767 if (!isset($from)) {
1768 $from = $this->count;
1769 }
1770
1771 $r = '/'.$regex.'/Ais';
1772 $result = preg_match($r, $this->buffer, $out, null, $from);
1773
1774 return $result;
1775 }
1776
1777 protected function seek($where = null)
1778 {
1779 if ($where === null) {
1780 return $this->count;
1781 }
1782
1783 $this->count = $where;
1784
1785 return true;
1786 }
1787
1788 public static function pregQuote($what)
1789 {
1790 return preg_quote($what, '/');
1791 }
1792
1793 protected function show()
1794 {
1795 if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
1796 return $m[1];
1797 }
1798
1799 return '';
1800 }
1801
1802 // turn list of length 1 into value type
1803 protected function flattenList($value)
1804 {
1805 if ($value[0] == 'list' && count($value[2]) == 1) {
1806 return $this->flattenList($value[2][0]);
1807 }
1808
1809 return $value;
1810 }
1811 }
1812