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