MDL-54934 workshop: ensure "Current phase" is always separated
[moodle.git] / lib / lessphp / Parser.php
blob86d455f1dda57a84e89ada1ea46b18531726ae53
1 <?php
3 require_once( dirname(__FILE__).'/Cache.php');
5 /**
6 * Class for parsing and compiling less files into css
8 * @package Less
9 * @subpackage parser
12 class Less_Parser{
15 /**
16 * Default parser options
18 public static $default_options = array(
19 'compress' => false, // option - whether to compress
20 'strictUnits' => false, // whether units need to evaluate correctly
21 'strictMath' => false, // whether math has to be within parenthesis
22 'relativeUrls' => true, // option - whether to adjust URL's to be relative
23 'urlArgs' => array(), // whether to add args into url tokens
24 'numPrecision' => 8,
26 'import_dirs' => array(),
27 'import_callback' => null,
28 'cache_dir' => null,
29 'cache_method' => 'php', // false, 'serialize', 'php', 'var_export', 'callback';
30 'cache_callback_get' => null,
31 'cache_callback_set' => null,
33 'sourceMap' => false, // whether to output a source map
34 'sourceMapBasepath' => null,
35 'sourceMapWriteTo' => null,
36 'sourceMapURL' => null,
38 'plugins' => array(),
42 public static $options = array();
45 private $input; // Less input string
46 private $input_len; // input string length
47 private $pos; // current index in `input`
48 private $saveStack = array(); // holds state for backtracking
49 private $furthest;
50 private $mb_internal_encoding = ''; // for remember exists value of mbstring.internal_encoding
52 /**
53 * @var Less_Environment
55 private $env;
57 protected $rules = array();
59 private static $imports = array();
61 public static $has_extends = false;
63 public static $next_id = 0;
65 /**
66 * Filename to contents of all parsed the files
68 * @var array
70 public static $contentsMap = array();
73 /**
74 * @param Less_Environment|array|null $env
76 public function __construct( $env = null ){
78 // Top parser on an import tree must be sure there is one "env"
79 // which will then be passed around by reference.
80 if( $env instanceof Less_Environment ){
81 $this->env = $env;
82 }else{
83 $this->SetOptions(Less_Parser::$default_options);
84 $this->Reset( $env );
87 // mbstring.func_overload > 1 bugfix
88 // The encoding value must be set for each source file,
89 // therefore, to conserve resources and improve the speed of this design is taken here
90 if (ini_get('mbstring.func_overload')) {
91 $this->mb_internal_encoding = ini_get('mbstring.internal_encoding');
92 @ini_set('mbstring.internal_encoding', 'ascii');
98 /**
99 * Reset the parser state completely
102 public function Reset( $options = null ){
103 $this->rules = array();
104 self::$imports = array();
105 self::$has_extends = false;
106 self::$imports = array();
107 self::$contentsMap = array();
109 $this->env = new Less_Environment($options);
110 $this->env->Init();
112 //set new options
113 if( is_array($options) ){
114 $this->SetOptions(Less_Parser::$default_options);
115 $this->SetOptions($options);
120 * Set one or more compiler options
121 * options: import_dirs, cache_dir, cache_method
124 public function SetOptions( $options ){
125 foreach($options as $option => $value){
126 $this->SetOption($option,$value);
131 * Set one compiler option
134 public function SetOption($option,$value){
136 switch($option){
138 case 'import_dirs':
139 $this->SetImportDirs($value);
140 return;
142 case 'cache_dir':
143 if( is_string($value) ){
144 Less_Cache::SetCacheDir($value);
145 Less_Cache::CheckCacheDir();
147 return;
150 Less_Parser::$options[$option] = $value;
154 * Registers a new custom function
156 * @param string $name function name
157 * @param callable $callback callback
159 public function registerFunction($name, $callback) {
160 $this->env->functions[$name] = $callback;
164 * Removed an already registered function
166 * @param string $name function name
168 public function unregisterFunction($name) {
169 if( isset($this->env->functions[$name]) )
170 unset($this->env->functions[$name]);
175 * Get the current css buffer
177 * @return string
179 public function getCss(){
181 $precision = ini_get('precision');
182 @ini_set('precision',16);
183 $locale = setlocale(LC_NUMERIC, 0);
184 setlocale(LC_NUMERIC, "C");
186 try {
188 $root = new Less_Tree_Ruleset(array(), $this->rules );
189 $root->root = true;
190 $root->firstRoot = true;
193 $this->PreVisitors($root);
195 self::$has_extends = false;
196 $evaldRoot = $root->compile($this->env);
200 $this->PostVisitors($evaldRoot);
202 if( Less_Parser::$options['sourceMap'] ){
203 $generator = new Less_SourceMap_Generator($evaldRoot, Less_Parser::$contentsMap, Less_Parser::$options );
204 // will also save file
205 // FIXME: should happen somewhere else?
206 $css = $generator->generateCSS();
207 }else{
208 $css = $evaldRoot->toCSS();
211 if( Less_Parser::$options['compress'] ){
212 $css = preg_replace('/(^(\s)+)|((\s)+$)/', '', $css);
215 } catch (Exception $exc) {
216 // Intentional fall-through so we can reset environment
219 //reset php settings
220 @ini_set('precision',$precision);
221 setlocale(LC_NUMERIC, $locale);
223 // If you previously defined $this->mb_internal_encoding
224 // is required to return the encoding as it was before
225 if ($this->mb_internal_encoding != '') {
226 @ini_set("mbstring.internal_encoding", $this->mb_internal_encoding);
227 $this->mb_internal_encoding = '';
230 // Rethrow exception after we handled resetting the environment
231 if (!empty($exc)) {
232 throw $exc;
237 return $css;
241 * Run pre-compile visitors
244 private function PreVisitors($root){
246 if( Less_Parser::$options['plugins'] ){
247 foreach(Less_Parser::$options['plugins'] as $plugin){
248 if( !empty($plugin->isPreEvalVisitor) ){
249 $plugin->run($root);
257 * Run post-compile visitors
260 private function PostVisitors($evaldRoot){
262 $visitors = array();
263 $visitors[] = new Less_Visitor_joinSelector();
264 if( self::$has_extends ){
265 $visitors[] = new Less_Visitor_processExtends();
267 $visitors[] = new Less_Visitor_toCSS();
270 if( Less_Parser::$options['plugins'] ){
271 foreach(Less_Parser::$options['plugins'] as $plugin){
272 if( property_exists($plugin,'isPreEvalVisitor') && $plugin->isPreEvalVisitor ){
273 continue;
276 if( property_exists($plugin,'isPreVisitor') && $plugin->isPreVisitor ){
277 array_unshift( $visitors, $plugin);
278 }else{
279 $visitors[] = $plugin;
285 for($i = 0; $i < count($visitors); $i++ ){
286 $visitors[$i]->run($evaldRoot);
293 * Parse a Less string into css
295 * @param string $str The string to convert
296 * @param string $uri_root The url of the file
297 * @return Less_Tree_Ruleset|Less_Parser
299 public function parse( $str, $file_uri = null ){
301 if( !$file_uri ){
302 $uri_root = '';
303 $filename = 'anonymous-file-'.Less_Parser::$next_id++.'.less';
304 }else{
305 $file_uri = self::WinPath($file_uri);
306 $filename = $file_uri;
307 $uri_root = dirname($file_uri);
310 $previousFileInfo = $this->env->currentFileInfo;
311 $uri_root = self::WinPath($uri_root);
312 $this->SetFileInfo($filename, $uri_root);
314 $this->input = $str;
315 $this->_parse();
317 if( $previousFileInfo ){
318 $this->env->currentFileInfo = $previousFileInfo;
321 return $this;
326 * Parse a Less string from a given file
328 * @throws Less_Exception_Parser
329 * @param string $filename The file to parse
330 * @param string $uri_root The url of the file
331 * @param bool $returnRoot Indicates whether the return value should be a css string a root node
332 * @return Less_Tree_Ruleset|Less_Parser
334 public function parseFile( $filename, $uri_root = '', $returnRoot = false){
336 if( !file_exists($filename) ){
337 $this->Error(sprintf('File `%s` not found.', $filename));
341 // fix uri_root?
342 // Instead of The mixture of file path for the first argument and directory path for the second argument has bee
343 if( !$returnRoot && !empty($uri_root) && basename($uri_root) == basename($filename) ){
344 $uri_root = dirname($uri_root);
348 $previousFileInfo = $this->env->currentFileInfo;
351 if( $filename ){
352 $filename = self::WinPath(realpath($filename));
354 $uri_root = self::WinPath($uri_root);
356 $this->SetFileInfo($filename, $uri_root);
358 self::AddParsedFile($filename);
360 if( $returnRoot ){
361 $rules = $this->GetRules( $filename );
362 $return = new Less_Tree_Ruleset(array(), $rules );
363 }else{
364 $this->_parse( $filename );
365 $return = $this;
368 if( $previousFileInfo ){
369 $this->env->currentFileInfo = $previousFileInfo;
372 return $return;
377 * Allows a user to set variables values
378 * @param array $vars
379 * @return Less_Parser
381 public function ModifyVars( $vars ){
383 $this->input = Less_Parser::serializeVars( $vars );
384 $this->_parse();
386 return $this;
391 * @param string $filename
393 public function SetFileInfo( $filename, $uri_root = ''){
395 $filename = Less_Environment::normalizePath($filename);
396 $dirname = preg_replace('/[^\/\\\\]*$/','',$filename);
398 if( !empty($uri_root) ){
399 $uri_root = rtrim($uri_root,'/').'/';
402 $currentFileInfo = array();
404 //entry info
405 if( isset($this->env->currentFileInfo) ){
406 $currentFileInfo['entryPath'] = $this->env->currentFileInfo['entryPath'];
407 $currentFileInfo['entryUri'] = $this->env->currentFileInfo['entryUri'];
408 $currentFileInfo['rootpath'] = $this->env->currentFileInfo['rootpath'];
410 }else{
411 $currentFileInfo['entryPath'] = $dirname;
412 $currentFileInfo['entryUri'] = $uri_root;
413 $currentFileInfo['rootpath'] = $dirname;
416 $currentFileInfo['currentDirectory'] = $dirname;
417 $currentFileInfo['currentUri'] = $uri_root.basename($filename);
418 $currentFileInfo['filename'] = $filename;
419 $currentFileInfo['uri_root'] = $uri_root;
422 //inherit reference
423 if( isset($this->env->currentFileInfo['reference']) && $this->env->currentFileInfo['reference'] ){
424 $currentFileInfo['reference'] = true;
427 $this->env->currentFileInfo = $currentFileInfo;
432 * @deprecated 1.5.1.2
435 public function SetCacheDir( $dir ){
437 if( !file_exists($dir) ){
438 if( mkdir($dir) ){
439 return true;
441 throw new Less_Exception_Parser('Less.php cache directory couldn\'t be created: '.$dir);
443 }elseif( !is_dir($dir) ){
444 throw new Less_Exception_Parser('Less.php cache directory doesn\'t exist: '.$dir);
446 }elseif( !is_writable($dir) ){
447 throw new Less_Exception_Parser('Less.php cache directory isn\'t writable: '.$dir);
449 }else{
450 $dir = self::WinPath($dir);
451 Less_Cache::$cache_dir = rtrim($dir,'/').'/';
452 return true;
458 * Set a list of directories or callbacks the parser should use for determining import paths
460 * @param array $dirs
462 public function SetImportDirs( $dirs ){
463 Less_Parser::$options['import_dirs'] = array();
465 foreach($dirs as $path => $uri_root){
467 $path = self::WinPath($path);
468 if( !empty($path) ){
469 $path = rtrim($path,'/').'/';
472 if ( !is_callable($uri_root) ){
473 $uri_root = self::WinPath($uri_root);
474 if( !empty($uri_root) ){
475 $uri_root = rtrim($uri_root,'/').'/';
479 Less_Parser::$options['import_dirs'][$path] = $uri_root;
484 * @param string $file_path
486 private function _parse( $file_path = null ){
487 $this->rules = array_merge($this->rules, $this->GetRules( $file_path ));
492 * Return the results of parsePrimary for $file_path
493 * Use cache and save cached results if possible
495 * @param string|null $file_path
497 private function GetRules( $file_path ){
499 $this->SetInput($file_path);
501 $cache_file = $this->CacheFile( $file_path );
502 if( $cache_file ){
503 if( Less_Parser::$options['cache_method'] == 'callback' ){
504 if( is_callable(Less_Parser::$options['cache_callback_get']) ){
505 $cache = call_user_func_array(
506 Less_Parser::$options['cache_callback_get'],
507 array($this, $file_path, $cache_file)
510 if( $cache ){
511 $this->UnsetInput();
512 return $cache;
516 }elseif( file_exists($cache_file) ){
517 switch(Less_Parser::$options['cache_method']){
519 // Using serialize
520 // Faster but uses more memory
521 case 'serialize':
522 $cache = unserialize(file_get_contents($cache_file));
523 if( $cache ){
524 touch($cache_file);
525 $this->UnsetInput();
526 return $cache;
528 break;
531 // Using generated php code
532 case 'var_export':
533 case 'php':
534 $this->UnsetInput();
535 return include($cache_file);
540 $rules = $this->parsePrimary();
542 if( $this->pos < $this->input_len ){
543 throw new Less_Exception_Chunk($this->input, null, $this->furthest, $this->env->currentFileInfo);
546 $this->UnsetInput();
549 //save the cache
550 if( $cache_file ){
551 if( Less_Parser::$options['cache_method'] == 'callback' ){
552 if( is_callable(Less_Parser::$options['cache_callback_set']) ){
553 call_user_func_array(
554 Less_Parser::$options['cache_callback_set'],
555 array($this, $file_path, $cache_file, $rules)
559 }else{
560 //msg('write cache file');
561 switch(Less_Parser::$options['cache_method']){
562 case 'serialize':
563 file_put_contents( $cache_file, serialize($rules) );
564 break;
565 case 'php':
566 file_put_contents( $cache_file, '<?php return '.self::ArgString($rules).'; ?>' );
567 break;
568 case 'var_export':
569 //Requires __set_state()
570 file_put_contents( $cache_file, '<?php return '.var_export($rules,true).'; ?>' );
571 break;
574 Less_Cache::CleanCache();
578 return $rules;
583 * Set up the input buffer
586 public function SetInput( $file_path ){
588 if( $file_path ){
589 $this->input = file_get_contents( $file_path );
592 $this->pos = $this->furthest = 0;
594 // Remove potential UTF Byte Order Mark
595 $this->input = preg_replace('/\\G\xEF\xBB\xBF/', '', $this->input);
596 $this->input_len = strlen($this->input);
599 if( Less_Parser::$options['sourceMap'] && $this->env->currentFileInfo ){
600 $uri = $this->env->currentFileInfo['currentUri'];
601 Less_Parser::$contentsMap[$uri] = $this->input;
608 * Free up some memory
611 public function UnsetInput(){
612 unset($this->input, $this->pos, $this->input_len, $this->furthest);
613 $this->saveStack = array();
617 public function CacheFile( $file_path ){
619 if( $file_path && $this->CacheEnabled() ){
621 $env = get_object_vars($this->env);
622 unset($env['frames']);
624 $parts = array();
625 $parts[] = $file_path;
626 $parts[] = filesize( $file_path );
627 $parts[] = filemtime( $file_path );
628 $parts[] = $env;
629 $parts[] = Less_Version::cache_version;
630 $parts[] = Less_Parser::$options['cache_method'];
631 return Less_Cache::$cache_dir . Less_Cache::$prefix . base_convert( sha1(json_encode($parts) ), 16, 36) . '.lesscache';
636 static function AddParsedFile($file){
637 self::$imports[] = $file;
640 static function AllParsedFiles(){
641 return self::$imports;
645 * @param string $file
647 static function FileParsed($file){
648 return in_array($file,self::$imports);
652 function save() {
653 $this->saveStack[] = $this->pos;
656 private function restore() {
657 $this->pos = array_pop($this->saveStack);
660 private function forget(){
661 array_pop($this->saveStack);
665 private function isWhitespace($offset = 0) {
666 return preg_match('/\s/',$this->input[ $this->pos + $offset]);
670 * Parse from a token, regexp or string, and move forward if match
672 * @param array $toks
673 * @return array
675 private function match($toks){
677 // The match is confirmed, add the match length to `this::pos`,
678 // and consume any extra white-space characters (' ' || '\n')
679 // which come after that. The reason for this is that LeSS's
680 // grammar is mostly white-space insensitive.
683 foreach($toks as $tok){
685 $char = $tok[0];
687 if( $char === '/' ){
688 $match = $this->MatchReg($tok);
690 if( $match ){
691 return count($match) === 1 ? $match[0] : $match;
694 }elseif( $char === '#' ){
695 $match = $this->MatchChar($tok[1]);
697 }else{
698 // Non-terminal, match using a function call
699 $match = $this->$tok();
703 if( $match ){
704 return $match;
710 * @param string[] $toks
712 * @return string
714 private function MatchFuncs($toks){
716 if( $this->pos < $this->input_len ){
717 foreach($toks as $tok){
718 $match = $this->$tok();
719 if( $match ){
720 return $match;
727 // Match a single character in the input,
728 private function MatchChar($tok){
729 if( ($this->pos < $this->input_len) && ($this->input[$this->pos] === $tok) ){
730 $this->skipWhitespace(1);
731 return $tok;
735 // Match a regexp from the current start point
736 private function MatchReg($tok){
738 if( preg_match($tok, $this->input, $match, 0, $this->pos) ){
739 $this->skipWhitespace(strlen($match[0]));
740 return $match;
746 * Same as match(), but don't change the state of the parser,
747 * just return the match.
749 * @param string $tok
750 * @return integer
752 public function PeekReg($tok){
753 return preg_match($tok, $this->input, $match, 0, $this->pos);
757 * @param string $tok
759 public function PeekChar($tok){
760 //return ($this->input[$this->pos] === $tok );
761 return ($this->pos < $this->input_len) && ($this->input[$this->pos] === $tok );
766 * @param integer $length
768 public function skipWhitespace($length){
770 $this->pos += $length;
772 for(; $this->pos < $this->input_len; $this->pos++ ){
773 $c = $this->input[$this->pos];
775 if( ($c !== "\n") && ($c !== "\r") && ($c !== "\t") && ($c !== ' ') ){
776 break;
783 * @param string $tok
784 * @param string|null $msg
786 public function expect($tok, $msg = NULL) {
787 $result = $this->match( array($tok) );
788 if (!$result) {
789 $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg );
790 } else {
791 return $result;
796 * @param string $tok
798 public function expectChar($tok, $msg = null ){
799 $result = $this->MatchChar($tok);
800 if( !$result ){
801 $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg );
802 }else{
803 return $result;
808 // Here in, the parsing rules/functions
810 // The basic structure of the syntax tree generated is as follows:
812 // Ruleset -> Rule -> Value -> Expression -> Entity
814 // Here's some LESS code:
816 // .class {
817 // color: #fff;
818 // border: 1px solid #000;
819 // width: @w + 4px;
820 // > .child {...}
821 // }
823 // And here's what the parse tree might look like:
825 // Ruleset (Selector '.class', [
826 // Rule ("color", Value ([Expression [Color #fff]]))
827 // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]]))
828 // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]]))
829 // Ruleset (Selector [Element '>', '.child'], [...])
830 // ])
832 // In general, most rules will try to parse a token with the `$()` function, and if the return
833 // value is truly, will return a new node, of the relevant type. Sometimes, we need to check
834 // first, before parsing, that's when we use `peek()`.
838 // The `primary` rule is the *entry* and *exit* point of the parser.
839 // The rules here can appear at any level of the parse tree.
841 // The recursive nature of the grammar is an interplay between the `block`
842 // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,
843 // as represented by this simplified grammar:
845 // primary → (ruleset | rule)+
846 // ruleset → selector+ block
847 // block → '{' primary '}'
849 // Only at one point is the primary rule not called from the
850 // block rule: at the root level.
852 private function parsePrimary(){
853 $root = array();
855 while( true ){
857 if( $this->pos >= $this->input_len ){
858 break;
861 $node = $this->parseExtend(true);
862 if( $node ){
863 $root = array_merge($root,$node);
864 continue;
867 //$node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseDirective'));
868 $node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseNameValue', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseRulesetCall', 'parseDirective'));
870 if( $node ){
871 $root[] = $node;
872 }elseif( !$this->MatchReg('/\\G[\s\n;]+/') ){
873 break;
876 if( $this->PeekChar('}') ){
877 break;
881 return $root;
886 // We create a Comment node for CSS comments `/* */`,
887 // but keep the LeSS comments `//` silent, by just skipping
888 // over them.
889 private function parseComment(){
891 if( $this->input[$this->pos] !== '/' ){
892 return;
895 if( $this->input[$this->pos+1] === '/' ){
896 $match = $this->MatchReg('/\\G\/\/.*/');
897 return $this->NewObj4('Less_Tree_Comment',array($match[0], true, $this->pos, $this->env->currentFileInfo));
900 //$comment = $this->MatchReg('/\\G\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/');
901 $comment = $this->MatchReg('/\\G\/\*(?s).*?\*+\/\n?/');//not the same as less.js to prevent fatal errors
902 if( $comment ){
903 return $this->NewObj4('Less_Tree_Comment',array($comment[0], false, $this->pos, $this->env->currentFileInfo));
907 private function parseComments(){
908 $comments = array();
910 while( $this->pos < $this->input_len ){
911 $comment = $this->parseComment();
912 if( !$comment ){
913 break;
916 $comments[] = $comment;
919 return $comments;
925 // A string, which supports escaping " and '
927 // "milky way" 'he\'s the one!'
929 private function parseEntitiesQuoted() {
930 $j = $this->pos;
931 $e = false;
932 $index = $this->pos;
934 if( $this->input[$this->pos] === '~' ){
935 $j++;
936 $e = true; // Escaped strings
939 if( $this->input[$j] != '"' && $this->input[$j] !== "'" ){
940 return;
943 if ($e) {
944 $this->MatchChar('~');
947 // Fix for #124: match escaped newlines
948 //$str = $this->MatchReg('/\\G"((?:[^"\\\\\r\n]|\\\\.)*)"|\'((?:[^\'\\\\\r\n]|\\\\.)*)\'/');
949 $str = $this->MatchReg('/\\G"((?:[^"\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)"|\'((?:[^\'\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)\'/');
951 if( $str ){
952 $result = $str[0][0] == '"' ? $str[1] : $str[2];
953 return $this->NewObj5('Less_Tree_Quoted',array($str[0], $result, $e, $index, $this->env->currentFileInfo) );
955 return;
960 // A catch-all word, such as:
962 // black border-collapse
964 private function parseEntitiesKeyword(){
966 //$k = $this->MatchReg('/\\G[_A-Za-z-][_A-Za-z0-9-]*/');
967 $k = $this->MatchReg('/\\G%|\\G[_A-Za-z-][_A-Za-z0-9-]*/');
968 if( $k ){
969 $k = $k[0];
970 $color = $this->fromKeyword($k);
971 if( $color ){
972 return $color;
974 return $this->NewObj1('Less_Tree_Keyword',$k);
978 // duplicate of Less_Tree_Color::FromKeyword
979 private function FromKeyword( $keyword ){
980 $keyword = strtolower($keyword);
982 if( Less_Colors::hasOwnProperty($keyword) ){
983 // detect named color
984 return $this->NewObj1('Less_Tree_Color',substr(Less_Colors::color($keyword), 1));
987 if( $keyword === 'transparent' ){
988 return $this->NewObj3('Less_Tree_Color', array( array(0, 0, 0), 0, true));
993 // A function call
995 // rgb(255, 0, 255)
997 // We also try to catch IE's `alpha()`, but let the `alpha` parser
998 // deal with the details.
1000 // The arguments are parsed with the `entities.arguments` parser.
1002 private function parseEntitiesCall(){
1003 $index = $this->pos;
1005 if( !preg_match('/\\G([\w-]+|%|progid:[\w\.]+)\(/', $this->input, $name,0,$this->pos) ){
1006 return;
1008 $name = $name[1];
1009 $nameLC = strtolower($name);
1011 if ($nameLC === 'url') {
1012 return null;
1015 $this->pos += strlen($name);
1017 if( $nameLC === 'alpha' ){
1018 $alpha_ret = $this->parseAlpha();
1019 if( $alpha_ret ){
1020 return $alpha_ret;
1024 $this->MatchChar('('); // Parse the '(' and consume whitespace.
1026 $args = $this->parseEntitiesArguments();
1028 if( !$this->MatchChar(')') ){
1029 return;
1032 if ($name) {
1033 return $this->NewObj4('Less_Tree_Call',array($name, $args, $index, $this->env->currentFileInfo) );
1038 * Parse a list of arguments
1040 * @return array
1042 private function parseEntitiesArguments(){
1044 $args = array();
1045 while( true ){
1046 $arg = $this->MatchFuncs( array('parseEntitiesAssignment','parseExpression') );
1047 if( !$arg ){
1048 break;
1051 $args[] = $arg;
1052 if( !$this->MatchChar(',') ){
1053 break;
1056 return $args;
1059 private function parseEntitiesLiteral(){
1060 return $this->MatchFuncs( array('parseEntitiesDimension','parseEntitiesColor','parseEntitiesQuoted','parseUnicodeDescriptor') );
1063 // Assignments are argument entities for calls.
1064 // They are present in ie filter properties as shown below.
1066 // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* )
1068 private function parseEntitiesAssignment() {
1070 $key = $this->MatchReg('/\\G\w+(?=\s?=)/');
1071 if( !$key ){
1072 return;
1075 if( !$this->MatchChar('=') ){
1076 return;
1079 $value = $this->parseEntity();
1080 if( $value ){
1081 return $this->NewObj2('Less_Tree_Assignment',array($key[0], $value));
1086 // Parse url() tokens
1088 // We use a specific rule for urls, because they don't really behave like
1089 // standard function calls. The difference is that the argument doesn't have
1090 // to be enclosed within a string, so it can't be parsed as an Expression.
1092 private function parseEntitiesUrl(){
1095 if( $this->input[$this->pos] !== 'u' || !$this->matchReg('/\\Gurl\(/') ){
1096 return;
1099 $value = $this->match( array('parseEntitiesQuoted','parseEntitiesVariable','/\\Gdata\:.*?[^\)]+/','/\\G(?:(?:\\\\[\(\)\'"])|[^\(\)\'"])+/') );
1100 if( !$value ){
1101 $value = '';
1105 $this->expectChar(')');
1108 if( isset($value->value) || $value instanceof Less_Tree_Variable ){
1109 return $this->NewObj2('Less_Tree_Url',array($value, $this->env->currentFileInfo));
1112 return $this->NewObj2('Less_Tree_Url', array( $this->NewObj1('Less_Tree_Anonymous',$value), $this->env->currentFileInfo) );
1117 // A Variable entity, such as `@fink`, in
1119 // width: @fink + 2px
1121 // We use a different parser for variable definitions,
1122 // see `parsers.variable`.
1124 private function parseEntitiesVariable(){
1125 $index = $this->pos;
1126 if ($this->PeekChar('@') && ($name = $this->MatchReg('/\\G@@?[\w-]+/'))) {
1127 return $this->NewObj3('Less_Tree_Variable', array( $name[0], $index, $this->env->currentFileInfo));
1132 // A variable entity useing the protective {} e.g. @{var}
1133 private function parseEntitiesVariableCurly() {
1134 $index = $this->pos;
1136 if( $this->input_len > ($this->pos+1) && $this->input[$this->pos] === '@' && ($curly = $this->MatchReg('/\\G@\{([\w-]+)\}/')) ){
1137 return $this->NewObj3('Less_Tree_Variable',array('@'.$curly[1], $index, $this->env->currentFileInfo));
1142 // A Hexadecimal color
1144 // #4F3C2F
1146 // `rgb` and `hsl` colors are parsed through the `entities.call` parser.
1148 private function parseEntitiesColor(){
1149 if ($this->PeekChar('#') && ($rgb = $this->MatchReg('/\\G#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/'))) {
1150 return $this->NewObj1('Less_Tree_Color',$rgb[1]);
1155 // A Dimension, that is, a number and a unit
1157 // 0.5em 95%
1159 private function parseEntitiesDimension(){
1161 $c = @ord($this->input[$this->pos]);
1163 //Is the first char of the dimension 0-9, '.', '+' or '-'
1164 if (($c > 57 || $c < 43) || $c === 47 || $c == 44){
1165 return;
1168 $value = $this->MatchReg('/\\G([+-]?\d*\.?\d+)(%|[a-z]+)?/');
1169 if( $value ){
1171 if( isset($value[2]) ){
1172 return $this->NewObj2('Less_Tree_Dimension', array($value[1],$value[2]));
1174 return $this->NewObj1('Less_Tree_Dimension',$value[1]);
1180 // A unicode descriptor, as is used in unicode-range
1182 // U+0?? or U+00A1-00A9
1184 function parseUnicodeDescriptor() {
1185 $ud = $this->MatchReg('/\\G(U\+[0-9a-fA-F?]+)(\-[0-9a-fA-F?]+)?/');
1186 if( $ud ){
1187 return $this->NewObj1('Less_Tree_UnicodeDescriptor', $ud[0]);
1193 // JavaScript code to be evaluated
1195 // `window.location.href`
1197 private function parseEntitiesJavascript(){
1198 $e = false;
1199 $j = $this->pos;
1200 if( $this->input[$j] === '~' ){
1201 $j++;
1202 $e = true;
1204 if( $this->input[$j] !== '`' ){
1205 return;
1207 if( $e ){
1208 $this->MatchChar('~');
1210 $str = $this->MatchReg('/\\G`([^`]*)`/');
1211 if( $str ){
1212 return $this->NewObj3('Less_Tree_Javascript', array($str[1], $this->pos, $e));
1218 // The variable part of a variable definition. Used in the `rule` parser
1220 // @fink:
1222 private function parseVariable(){
1223 if ($this->PeekChar('@') && ($name = $this->MatchReg('/\\G(@[\w-]+)\s*:/'))) {
1224 return $name[1];
1230 // The variable part of a variable definition. Used in the `rule` parser
1232 // @fink();
1234 private function parseRulesetCall(){
1236 if( $this->input[$this->pos] === '@' && ($name = $this->MatchReg('/\\G(@[\w-]+)\s*\(\s*\)\s*;/')) ){
1237 return $this->NewObj1('Less_Tree_RulesetCall', $name[1] );
1243 // extend syntax - used to extend selectors
1245 function parseExtend($isRule = false){
1247 $index = $this->pos;
1248 $extendList = array();
1251 if( !$this->MatchReg( $isRule ? '/\\G&:extend\(/' : '/\\G:extend\(/' ) ){ return; }
1254 $option = null;
1255 $elements = array();
1256 while( true ){
1257 $option = $this->MatchReg('/\\G(all)(?=\s*(\)|,))/');
1258 if( $option ){ break; }
1259 $e = $this->parseElement();
1260 if( !$e ){ break; }
1261 $elements[] = $e;
1264 if( $option ){
1265 $option = $option[1];
1268 $extendList[] = $this->NewObj3('Less_Tree_Extend', array( $this->NewObj1('Less_Tree_Selector',$elements), $option, $index ));
1270 }while( $this->MatchChar(",") );
1272 $this->expect('/\\G\)/');
1274 if( $isRule ){
1275 $this->expect('/\\G;/');
1278 return $extendList;
1283 // A Mixin call, with an optional argument list
1285 // #mixins > .square(#fff);
1286 // .rounded(4px, black);
1287 // .button;
1289 // The `while` loop is there because mixins can be
1290 // namespaced, but we only support the child and descendant
1291 // selector for now.
1293 private function parseMixinCall(){
1295 $char = $this->input[$this->pos];
1296 if( $char !== '.' && $char !== '#' ){
1297 return;
1300 $index = $this->pos;
1301 $this->save(); // stop us absorbing part of an invalid selector
1303 $elements = $this->parseMixinCallElements();
1305 if( $elements ){
1307 if( $this->MatchChar('(') ){
1308 $returned = $this->parseMixinArgs(true);
1309 $args = $returned['args'];
1310 $this->expectChar(')');
1311 }else{
1312 $args = array();
1315 $important = $this->parseImportant();
1317 if( $this->parseEnd() ){
1318 $this->forget();
1319 return $this->NewObj5('Less_Tree_Mixin_Call', array( $elements, $args, $index, $this->env->currentFileInfo, $important));
1323 $this->restore();
1327 private function parseMixinCallElements(){
1328 $elements = array();
1329 $c = null;
1331 while( true ){
1332 $elemIndex = $this->pos;
1333 $e = $this->MatchReg('/\\G[#.](?:[\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/');
1334 if( !$e ){
1335 break;
1337 $elements[] = $this->NewObj4('Less_Tree_Element', array($c, $e[0], $elemIndex, $this->env->currentFileInfo));
1338 $c = $this->MatchChar('>');
1341 return $elements;
1347 * @param boolean $isCall
1349 private function parseMixinArgs( $isCall ){
1350 $expressions = array();
1351 $argsSemiColon = array();
1352 $isSemiColonSeperated = null;
1353 $argsComma = array();
1354 $expressionContainsNamed = null;
1355 $name = null;
1356 $returner = array('args'=>array(), 'variadic'=> false);
1358 $this->save();
1360 while( true ){
1361 if( $isCall ){
1362 $arg = $this->MatchFuncs( array( 'parseDetachedRuleset','parseExpression' ) );
1363 } else {
1364 $this->parseComments();
1365 if( $this->input[ $this->pos ] === '.' && $this->MatchReg('/\\G\.{3}/') ){
1366 $returner['variadic'] = true;
1367 if( $this->MatchChar(";") && !$isSemiColonSeperated ){
1368 $isSemiColonSeperated = true;
1371 if( $isSemiColonSeperated ){
1372 $argsSemiColon[] = array('variadic'=>true);
1373 }else{
1374 $argsComma[] = array('variadic'=>true);
1376 break;
1378 $arg = $this->MatchFuncs( array('parseEntitiesVariable','parseEntitiesLiteral','parseEntitiesKeyword') );
1381 if( !$arg ){
1382 break;
1386 $nameLoop = null;
1387 if( $arg instanceof Less_Tree_Expression ){
1388 $arg->throwAwayComments();
1390 $value = $arg;
1391 $val = null;
1393 if( $isCall ){
1394 // Variable
1395 if( property_exists($arg,'value') && count($arg->value) == 1 ){
1396 $val = $arg->value[0];
1398 } else {
1399 $val = $arg;
1403 if( $val instanceof Less_Tree_Variable ){
1405 if( $this->MatchChar(':') ){
1406 if( $expressions ){
1407 if( $isSemiColonSeperated ){
1408 $this->Error('Cannot mix ; and , as delimiter types');
1410 $expressionContainsNamed = true;
1413 // we do not support setting a ruleset as a default variable - it doesn't make sense
1414 // However if we do want to add it, there is nothing blocking it, just don't error
1415 // and remove isCall dependency below
1416 $value = null;
1417 if( $isCall ){
1418 $value = $this->parseDetachedRuleset();
1420 if( !$value ){
1421 $value = $this->parseExpression();
1424 if( !$value ){
1425 if( $isCall ){
1426 $this->Error('could not understand value for named argument');
1427 } else {
1428 $this->restore();
1429 $returner['args'] = array();
1430 return $returner;
1434 $nameLoop = ($name = $val->name);
1435 }elseif( !$isCall && $this->MatchReg('/\\G\.{3}/') ){
1436 $returner['variadic'] = true;
1437 if( $this->MatchChar(";") && !$isSemiColonSeperated ){
1438 $isSemiColonSeperated = true;
1440 if( $isSemiColonSeperated ){
1441 $argsSemiColon[] = array('name'=> $arg->name, 'variadic' => true);
1442 }else{
1443 $argsComma[] = array('name'=> $arg->name, 'variadic' => true);
1445 break;
1446 }elseif( !$isCall ){
1447 $name = $nameLoop = $val->name;
1448 $value = null;
1452 if( $value ){
1453 $expressions[] = $value;
1456 $argsComma[] = array('name'=>$nameLoop, 'value'=>$value );
1458 if( $this->MatchChar(',') ){
1459 continue;
1462 if( $this->MatchChar(';') || $isSemiColonSeperated ){
1464 if( $expressionContainsNamed ){
1465 $this->Error('Cannot mix ; and , as delimiter types');
1468 $isSemiColonSeperated = true;
1470 if( count($expressions) > 1 ){
1471 $value = $this->NewObj1('Less_Tree_Value', $expressions);
1473 $argsSemiColon[] = array('name'=>$name, 'value'=>$value );
1475 $name = null;
1476 $expressions = array();
1477 $expressionContainsNamed = false;
1481 $this->forget();
1482 $returner['args'] = ($isSemiColonSeperated ? $argsSemiColon : $argsComma);
1483 return $returner;
1489 // A Mixin definition, with a list of parameters
1491 // .rounded (@radius: 2px, @color) {
1492 // ...
1493 // }
1495 // Until we have a finer grained state-machine, we have to
1496 // do a look-ahead, to make sure we don't have a mixin call.
1497 // See the `rule` function for more information.
1499 // We start by matching `.rounded (`, and then proceed on to
1500 // the argument list, which has optional default values.
1501 // We store the parameters in `params`, with a `value` key,
1502 // if there is a value, such as in the case of `@radius`.
1504 // Once we've got our params list, and a closing `)`, we parse
1505 // the `{...}` block.
1507 private function parseMixinDefinition(){
1508 $cond = null;
1510 $char = $this->input[$this->pos];
1511 if( ($char !== '.' && $char !== '#') || ($char === '{' && $this->PeekReg('/\\G[^{]*\}/')) ){
1512 return;
1515 $this->save();
1517 $match = $this->MatchReg('/\\G([#.](?:[\w-]|\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/');
1518 if( $match ){
1519 $name = $match[1];
1521 $argInfo = $this->parseMixinArgs( false );
1522 $params = $argInfo['args'];
1523 $variadic = $argInfo['variadic'];
1526 // .mixincall("@{a}");
1527 // looks a bit like a mixin definition..
1528 // also
1529 // .mixincall(@a: {rule: set;});
1530 // so we have to be nice and restore
1531 if( !$this->MatchChar(')') ){
1532 $this->furthest = $this->pos;
1533 $this->restore();
1534 return;
1538 $this->parseComments();
1540 if ($this->MatchReg('/\\Gwhen/')) { // Guard
1541 $cond = $this->expect('parseConditions', 'Expected conditions');
1544 $ruleset = $this->parseBlock();
1546 if( is_array($ruleset) ){
1547 $this->forget();
1548 return $this->NewObj5('Less_Tree_Mixin_Definition', array( $name, $params, $ruleset, $cond, $variadic));
1551 $this->restore();
1552 }else{
1553 $this->forget();
1558 // Entities are the smallest recognized token,
1559 // and can be found inside a rule's value.
1561 private function parseEntity(){
1563 return $this->MatchFuncs( array('parseEntitiesLiteral','parseEntitiesVariable','parseEntitiesUrl','parseEntitiesCall','parseEntitiesKeyword','parseEntitiesJavascript','parseComment') );
1567 // A Rule terminator. Note that we use `peek()` to check for '}',
1568 // because the `block` rule will be expecting it, but we still need to make sure
1569 // it's there, if ';' was ommitted.
1571 private function parseEnd(){
1572 return $this->MatchChar(';') || $this->PeekChar('}');
1576 // IE's alpha function
1578 // alpha(opacity=88)
1580 private function parseAlpha(){
1582 if ( ! $this->MatchReg('/\\G\(opacity=/i')) {
1583 return;
1586 $value = $this->MatchReg('/\\G[0-9]+/');
1587 if( $value ){
1588 $value = $value[0];
1589 }else{
1590 $value = $this->parseEntitiesVariable();
1591 if( !$value ){
1592 return;
1596 $this->expectChar(')');
1597 return $this->NewObj1('Less_Tree_Alpha',$value);
1602 // A Selector Element
1604 // div
1605 // + h1
1606 // #socks
1607 // input[type="text"]
1609 // Elements are the building blocks for Selectors,
1610 // they are made out of a `Combinator` (see combinator rule),
1611 // and an element name, such as a tag a class, or `*`.
1613 private function parseElement(){
1614 $c = $this->parseCombinator();
1615 $index = $this->pos;
1617 $e = $this->match( array('/\\G(?:\d+\.\d+|\d+)%/', '/\\G(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/',
1618 '#*', '#&', 'parseAttribute', '/\\G\([^()@]+\)/', '/\\G[\.#](?=@)/', 'parseEntitiesVariableCurly') );
1620 if( is_null($e) ){
1621 $this->save();
1622 if( $this->MatchChar('(') ){
1623 if( ($v = $this->parseSelector()) && $this->MatchChar(')') ){
1624 $e = $this->NewObj1('Less_Tree_Paren',$v);
1625 $this->forget();
1626 }else{
1627 $this->restore();
1629 }else{
1630 $this->forget();
1634 if( !is_null($e) ){
1635 return $this->NewObj4('Less_Tree_Element',array( $c, $e, $index, $this->env->currentFileInfo));
1640 // Combinators combine elements together, in a Selector.
1642 // Because our parser isn't white-space sensitive, special care
1643 // has to be taken, when parsing the descendant combinator, ` `,
1644 // as it's an empty space. We have to check the previous character
1645 // in the input, to see if it's a ` ` character.
1647 private function parseCombinator(){
1648 if( $this->pos < $this->input_len ){
1649 $c = $this->input[$this->pos];
1650 if ($c === '>' || $c === '+' || $c === '~' || $c === '|' || $c === '^' ){
1652 $this->pos++;
1653 if( $this->input[$this->pos] === '^' ){
1654 $c = '^^';
1655 $this->pos++;
1658 $this->skipWhitespace(0);
1660 return $c;
1663 if( $this->pos > 0 && $this->isWhitespace(-1) ){
1664 return ' ';
1670 // A CSS selector (see selector below)
1671 // with less extensions e.g. the ability to extend and guard
1673 private function parseLessSelector(){
1674 return $this->parseSelector(true);
1678 // A CSS Selector
1680 // .class > div + h1
1681 // li a:hover
1683 // Selectors are made out of one or more Elements, see above.
1685 private function parseSelector( $isLess = false ){
1686 $elements = array();
1687 $extendList = array();
1688 $condition = null;
1689 $when = false;
1690 $extend = false;
1691 $e = null;
1692 $c = null;
1693 $index = $this->pos;
1695 while( ($isLess && ($extend = $this->parseExtend())) || ($isLess && ($when = $this->MatchReg('/\\Gwhen/') )) || ($e = $this->parseElement()) ){
1696 if( $when ){
1697 $condition = $this->expect('parseConditions', 'expected condition');
1698 }elseif( $condition ){
1699 //error("CSS guard can only be used at the end of selector");
1700 }elseif( $extend ){
1701 $extendList = array_merge($extendList,$extend);
1702 }else{
1703 //if( count($extendList) ){
1704 //error("Extend can only be used at the end of selector");
1706 if( $this->pos < $this->input_len ){
1707 $c = $this->input[ $this->pos ];
1709 $elements[] = $e;
1710 $e = null;
1713 if( $c === '{' || $c === '}' || $c === ';' || $c === ',' || $c === ')') { break; }
1716 if( $elements ){
1717 return $this->NewObj5('Less_Tree_Selector',array($elements, $extendList, $condition, $index, $this->env->currentFileInfo));
1719 if( $extendList ) {
1720 $this->Error('Extend must be used to extend a selector, it cannot be used on its own');
1724 private function parseTag(){
1725 return ( $tag = $this->MatchReg('/\\G[A-Za-z][A-Za-z-]*[0-9]?/') ) ? $tag : $this->MatchChar('*');
1728 private function parseAttribute(){
1730 $val = null;
1732 if( !$this->MatchChar('[') ){
1733 return;
1736 $key = $this->parseEntitiesVariableCurly();
1737 if( !$key ){
1738 $key = $this->expect('/\\G(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\\\.)+/');
1741 $op = $this->MatchReg('/\\G[|~*$^]?=/');
1742 if( $op ){
1743 $val = $this->match( array('parseEntitiesQuoted','/\\G[0-9]+%/','/\\G[\w-]+/','parseEntitiesVariableCurly') );
1746 $this->expectChar(']');
1748 return $this->NewObj3('Less_Tree_Attribute',array( $key, $op[0], $val));
1752 // The `block` rule is used by `ruleset` and `mixin.definition`.
1753 // It's a wrapper around the `primary` rule, with added `{}`.
1755 private function parseBlock(){
1756 if( $this->MatchChar('{') ){
1757 $content = $this->parsePrimary();
1758 if( $this->MatchChar('}') ){
1759 return $content;
1764 private function parseBlockRuleset(){
1765 $block = $this->parseBlock();
1767 if( $block ){
1768 $block = $this->NewObj2('Less_Tree_Ruleset',array( null, $block));
1771 return $block;
1774 private function parseDetachedRuleset(){
1775 $blockRuleset = $this->parseBlockRuleset();
1776 if( $blockRuleset ){
1777 return $this->NewObj1('Less_Tree_DetachedRuleset',$blockRuleset);
1782 // div, .class, body > p {...}
1784 private function parseRuleset(){
1785 $selectors = array();
1787 $this->save();
1789 while( true ){
1790 $s = $this->parseLessSelector();
1791 if( !$s ){
1792 break;
1794 $selectors[] = $s;
1795 $this->parseComments();
1797 if( $s->condition && count($selectors) > 1 ){
1798 $this->Error('Guards are only currently allowed on a single selector.');
1801 if( !$this->MatchChar(',') ){
1802 break;
1804 if( $s->condition ){
1805 $this->Error('Guards are only currently allowed on a single selector.');
1807 $this->parseComments();
1811 if( $selectors ){
1812 $rules = $this->parseBlock();
1813 if( is_array($rules) ){
1814 $this->forget();
1815 return $this->NewObj2('Less_Tree_Ruleset',array( $selectors, $rules)); //Less_Environment::$strictImports
1819 // Backtrack
1820 $this->furthest = $this->pos;
1821 $this->restore();
1825 * Custom less.php parse function for finding simple name-value css pairs
1826 * ex: width:100px;
1829 private function parseNameValue(){
1831 $index = $this->pos;
1832 $this->save();
1835 //$match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*((?:\'")?[a-zA-Z0-9\-% \.,!]+?(?:\'")?)\s*([;}])/');
1836 $match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*([\'"]?[#a-zA-Z0-9\-%\.,]+?[\'"]?) *(! *important)?\s*([;}])/');
1837 if( $match ){
1839 if( $match[4] == '}' ){
1840 $this->pos = $index + strlen($match[0])-1;
1843 if( $match[3] ){
1844 $match[2] .= ' !important';
1847 return $this->NewObj4('Less_Tree_NameValue',array( $match[1], $match[2], $index, $this->env->currentFileInfo));
1850 $this->restore();
1854 private function parseRule( $tryAnonymous = null ){
1856 $merge = false;
1857 $startOfRule = $this->pos;
1859 $c = $this->input[$this->pos];
1860 if( $c === '.' || $c === '#' || $c === '&' ){
1861 return;
1864 $this->save();
1865 $name = $this->MatchFuncs( array('parseVariable','parseRuleProperty'));
1867 if( $name ){
1869 $isVariable = is_string($name);
1871 $value = null;
1872 if( $isVariable ){
1873 $value = $this->parseDetachedRuleset();
1876 $important = null;
1877 if( !$value ){
1879 // prefer to try to parse first if its a variable or we are compressing
1880 // but always fallback on the other one
1881 //if( !$tryAnonymous && is_string($name) && $name[0] === '@' ){
1882 if( !$tryAnonymous && (Less_Parser::$options['compress'] || $isVariable) ){
1883 $value = $this->MatchFuncs( array('parseValue','parseAnonymousValue'));
1884 }else{
1885 $value = $this->MatchFuncs( array('parseAnonymousValue','parseValue'));
1888 $important = $this->parseImportant();
1890 // a name returned by this.ruleProperty() is always an array of the form:
1891 // [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"]
1892 // where each item is a tree.Keyword or tree.Variable
1893 if( !$isVariable && is_array($name) ){
1894 $nm = array_pop($name);
1895 if( $nm->value ){
1896 $merge = $nm->value;
1902 if( $value && $this->parseEnd() ){
1903 $this->forget();
1904 return $this->NewObj6('Less_Tree_Rule',array( $name, $value, $important, $merge, $startOfRule, $this->env->currentFileInfo));
1905 }else{
1906 $this->furthest = $this->pos;
1907 $this->restore();
1908 if( $value && !$tryAnonymous ){
1909 return $this->parseRule(true);
1912 }else{
1913 $this->forget();
1917 function parseAnonymousValue(){
1919 if( preg_match('/\\G([^@+\/\'"*`(;{}-]*);/',$this->input, $match, 0, $this->pos) ){
1920 $this->pos += strlen($match[1]);
1921 return $this->NewObj1('Less_Tree_Anonymous',$match[1]);
1926 // An @import directive
1928 // @import "lib";
1930 // Depending on our environment, importing is done differently:
1931 // In the browser, it's an XHR request, in Node, it would be a
1932 // file-system operation. The function used for importing is
1933 // stored in `import`, which we pass to the Import constructor.
1935 private function parseImport(){
1937 $this->save();
1939 $dir = $this->MatchReg('/\\G@import?\s+/');
1941 if( $dir ){
1942 $options = $this->parseImportOptions();
1943 $path = $this->MatchFuncs( array('parseEntitiesQuoted','parseEntitiesUrl'));
1945 if( $path ){
1946 $features = $this->parseMediaFeatures();
1947 if( $this->MatchChar(';') ){
1948 if( $features ){
1949 $features = $this->NewObj1('Less_Tree_Value',$features);
1952 $this->forget();
1953 return $this->NewObj5('Less_Tree_Import',array( $path, $features, $options, $this->pos, $this->env->currentFileInfo));
1958 $this->restore();
1961 private function parseImportOptions(){
1963 $options = array();
1965 // list of options, surrounded by parens
1966 if( !$this->MatchChar('(') ){
1967 return $options;
1970 $optionName = $this->parseImportOption();
1971 if( $optionName ){
1972 $value = true;
1973 switch( $optionName ){
1974 case "css":
1975 $optionName = "less";
1976 $value = false;
1977 break;
1978 case "once":
1979 $optionName = "multiple";
1980 $value = false;
1981 break;
1983 $options[$optionName] = $value;
1984 if( !$this->MatchChar(',') ){ break; }
1986 }while( $optionName );
1987 $this->expectChar(')');
1988 return $options;
1991 private function parseImportOption(){
1992 $opt = $this->MatchReg('/\\G(less|css|multiple|once|inline|reference)/');
1993 if( $opt ){
1994 return $opt[1];
1998 private function parseMediaFeature() {
1999 $nodes = array();
2002 $e = $this->MatchFuncs(array('parseEntitiesKeyword','parseEntitiesVariable'));
2003 if( $e ){
2004 $nodes[] = $e;
2005 } elseif ($this->MatchChar('(')) {
2006 $p = $this->parseProperty();
2007 $e = $this->parseValue();
2008 if ($this->MatchChar(')')) {
2009 if ($p && $e) {
2010 $r = $this->NewObj7('Less_Tree_Rule', array( $p, $e, null, null, $this->pos, $this->env->currentFileInfo, true));
2011 $nodes[] = $this->NewObj1('Less_Tree_Paren',$r);
2012 } elseif ($e) {
2013 $nodes[] = $this->NewObj1('Less_Tree_Paren',$e);
2014 } else {
2015 return null;
2017 } else
2018 return null;
2020 } while ($e);
2022 if ($nodes) {
2023 return $this->NewObj1('Less_Tree_Expression',$nodes);
2027 private function parseMediaFeatures() {
2028 $features = array();
2031 $e = $this->parseMediaFeature();
2032 if( $e ){
2033 $features[] = $e;
2034 if (!$this->MatchChar(',')) break;
2035 }else{
2036 $e = $this->parseEntitiesVariable();
2037 if( $e ){
2038 $features[] = $e;
2039 if (!$this->MatchChar(',')) break;
2042 } while ($e);
2044 return $features ? $features : null;
2047 private function parseMedia() {
2048 if( $this->MatchReg('/\\G@media/') ){
2049 $features = $this->parseMediaFeatures();
2050 $rules = $this->parseBlock();
2052 if( is_array($rules) ){
2053 return $this->NewObj4('Less_Tree_Media',array( $rules, $features, $this->pos, $this->env->currentFileInfo));
2060 // A CSS Directive
2062 // @charset "utf-8";
2064 private function parseDirective(){
2066 if( !$this->PeekChar('@') ){
2067 return;
2070 $rules = null;
2071 $index = $this->pos;
2072 $hasBlock = true;
2073 $hasIdentifier = false;
2074 $hasExpression = false;
2075 $hasUnknown = false;
2078 $value = $this->MatchFuncs(array('parseImport','parseMedia'));
2079 if( $value ){
2080 return $value;
2083 $this->save();
2085 $name = $this->MatchReg('/\\G@[a-z-]+/');
2087 if( !$name ) return;
2088 $name = $name[0];
2091 $nonVendorSpecificName = $name;
2092 $pos = strpos($name,'-', 2);
2093 if( $name[1] == '-' && $pos > 0 ){
2094 $nonVendorSpecificName = "@" . substr($name, $pos + 1);
2098 switch( $nonVendorSpecificName ){
2100 case "@font-face":
2101 case "@viewport":
2102 case "@top-left":
2103 case "@top-left-corner":
2104 case "@top-center":
2105 case "@top-right":
2106 case "@top-right-corner":
2107 case "@bottom-left":
2108 case "@bottom-left-corner":
2109 case "@bottom-center":
2110 case "@bottom-right":
2111 case "@bottom-right-corner":
2112 case "@left-top":
2113 case "@left-middle":
2114 case "@left-bottom":
2115 case "@right-top":
2116 case "@right-middle":
2117 case "@right-bottom":
2118 hasBlock = true;
2119 break;
2121 case "@charset":
2122 $hasIdentifier = true;
2123 $hasBlock = false;
2124 break;
2125 case "@namespace":
2126 $hasExpression = true;
2127 $hasBlock = false;
2128 break;
2129 case "@keyframes":
2130 $hasIdentifier = true;
2131 break;
2132 case "@host":
2133 case "@page":
2134 case "@document":
2135 case "@supports":
2136 $hasUnknown = true;
2137 break;
2140 if( $hasIdentifier ){
2141 $value = $this->parseEntity();
2142 if( !$value ){
2143 $this->error("expected " . $name . " identifier");
2145 } else if( $hasExpression ){
2146 $value = $this->parseExpression();
2147 if( !$value ){
2148 $this->error("expected " . $name. " expression");
2150 } else if ($hasUnknown) {
2152 $value = $this->MatchReg('/\\G[^{;]+/');
2153 if( $value ){
2154 $value = $this->NewObj1('Less_Tree_Anonymous',trim($value[0]));
2158 if( $hasBlock ){
2159 $rules = $this->parseBlockRuleset();
2162 if( $rules || (!$hasBlock && $value && $this->MatchChar(';'))) {
2163 $this->forget();
2164 return $this->NewObj5('Less_Tree_Directive',array($name, $value, $rules, $index, $this->env->currentFileInfo));
2167 $this->restore();
2172 // A Value is a comma-delimited list of Expressions
2174 // font-family: Baskerville, Georgia, serif;
2176 // In a Rule, a Value represents everything after the `:`,
2177 // and before the `;`.
2179 private function parseValue(){
2180 $expressions = array();
2183 $e = $this->parseExpression();
2184 if( $e ){
2185 $expressions[] = $e;
2186 if (! $this->MatchChar(',')) {
2187 break;
2190 }while($e);
2192 if( $expressions ){
2193 return $this->NewObj1('Less_Tree_Value',$expressions);
2197 private function parseImportant (){
2198 if( $this->PeekChar('!') && $this->MatchReg('/\\G! *important/') ){
2199 return ' !important';
2203 private function parseSub (){
2205 if( $this->MatchChar('(') ){
2206 $a = $this->parseAddition();
2207 if( $a ){
2208 $this->expectChar(')');
2209 return $this->NewObj2('Less_Tree_Expression',array( array($a), true) ); //instead of $e->parens = true so the value is cached
2216 * Parses multiplication operation
2218 * @return Less_Tree_Operation|null
2220 function parseMultiplication(){
2222 $return = $m = $this->parseOperand();
2223 if( $return ){
2224 while( true ){
2226 $isSpaced = $this->isWhitespace( -1 );
2228 if( $this->PeekReg('/\\G\/[*\/]/') ){
2229 break;
2232 $op = $this->MatchChar('/');
2233 if( !$op ){
2234 $op = $this->MatchChar('*');
2235 if( !$op ){
2236 break;
2240 $a = $this->parseOperand();
2242 if(!$a) { break; }
2244 $m->parensInOp = true;
2245 $a->parensInOp = true;
2246 $return = $this->NewObj3('Less_Tree_Operation',array( $op, array( $return, $a ), $isSpaced) );
2249 return $return;
2255 * Parses an addition operation
2257 * @return Less_Tree_Operation|null
2259 private function parseAddition (){
2261 $return = $m = $this->parseMultiplication();
2262 if( $return ){
2263 while( true ){
2265 $isSpaced = $this->isWhitespace( -1 );
2267 $op = $this->MatchReg('/\\G[-+]\s+/');
2268 if( $op ){
2269 $op = $op[0];
2270 }else{
2271 if( !$isSpaced ){
2272 $op = $this->match(array('#+','#-'));
2274 if( !$op ){
2275 break;
2279 $a = $this->parseMultiplication();
2280 if( !$a ){
2281 break;
2284 $m->parensInOp = true;
2285 $a->parensInOp = true;
2286 $return = $this->NewObj3('Less_Tree_Operation',array($op, array($return, $a), $isSpaced));
2290 return $return;
2295 * Parses the conditions
2297 * @return Less_Tree_Condition|null
2299 private function parseConditions() {
2300 $index = $this->pos;
2301 $return = $a = $this->parseCondition();
2302 if( $a ){
2303 while( true ){
2304 if( !$this->PeekReg('/\\G,\s*(not\s*)?\(/') || !$this->MatchChar(',') ){
2305 break;
2307 $b = $this->parseCondition();
2308 if( !$b ){
2309 break;
2312 $return = $this->NewObj4('Less_Tree_Condition',array('or', $return, $b, $index));
2314 return $return;
2318 private function parseCondition() {
2319 $index = $this->pos;
2320 $negate = false;
2321 $c = null;
2323 if ($this->MatchReg('/\\Gnot/')) $negate = true;
2324 $this->expectChar('(');
2325 $a = $this->MatchFuncs(array('parseAddition','parseEntitiesKeyword','parseEntitiesQuoted'));
2327 if( $a ){
2328 $op = $this->MatchReg('/\\G(?:>=|<=|=<|[<=>])/');
2329 if( $op ){
2330 $b = $this->MatchFuncs(array('parseAddition','parseEntitiesKeyword','parseEntitiesQuoted'));
2331 if( $b ){
2332 $c = $this->NewObj5('Less_Tree_Condition',array($op[0], $a, $b, $index, $negate));
2333 } else {
2334 $this->Error('Unexpected expression');
2336 } else {
2337 $k = $this->NewObj1('Less_Tree_Keyword','true');
2338 $c = $this->NewObj5('Less_Tree_Condition',array('=', $a, $k, $index, $negate));
2340 $this->expectChar(')');
2341 return $this->MatchReg('/\\Gand/') ? $this->NewObj3('Less_Tree_Condition',array('and', $c, $this->parseCondition())) : $c;
2346 * An operand is anything that can be part of an operation,
2347 * such as a Color, or a Variable
2350 private function parseOperand (){
2352 $negate = false;
2353 $offset = $this->pos+1;
2354 if( $offset >= $this->input_len ){
2355 return;
2357 $char = $this->input[$offset];
2358 if( $char === '@' || $char === '(' ){
2359 $negate = $this->MatchChar('-');
2362 $o = $this->MatchFuncs(array('parseSub','parseEntitiesDimension','parseEntitiesColor','parseEntitiesVariable','parseEntitiesCall'));
2364 if( $negate ){
2365 $o->parensInOp = true;
2366 $o = $this->NewObj1('Less_Tree_Negative',$o);
2369 return $o;
2374 * Expressions either represent mathematical operations,
2375 * or white-space delimited Entities.
2377 * 1px solid black
2378 * @var * 2
2380 * @return Less_Tree_Expression|null
2382 private function parseExpression (){
2383 $entities = array();
2386 $e = $this->MatchFuncs(array('parseAddition','parseEntity'));
2387 if( $e ){
2388 $entities[] = $e;
2389 // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here
2390 if( !$this->PeekReg('/\\G\/[\/*]/') ){
2391 $delim = $this->MatchChar('/');
2392 if( $delim ){
2393 $entities[] = $this->NewObj1('Less_Tree_Anonymous',$delim);
2397 }while($e);
2399 if( $entities ){
2400 return $this->NewObj1('Less_Tree_Expression',$entities);
2406 * Parse a property
2407 * eg: 'min-width', 'orientation', etc
2409 * @return string
2411 private function parseProperty (){
2412 $name = $this->MatchReg('/\\G(\*?-?[_a-zA-Z0-9-]+)\s*:/');
2413 if( $name ){
2414 return $name[1];
2420 * Parse a rule property
2421 * eg: 'color', 'width', 'height', etc
2423 * @return string
2425 private function parseRuleProperty(){
2426 $offset = $this->pos;
2427 $name = array();
2428 $index = array();
2429 $length = 0;
2432 $this->rulePropertyMatch('/\\G(\*?)/', $offset, $length, $index, $name );
2433 while( $this->rulePropertyMatch('/\\G((?:[\w-]+)|(?:@\{[\w-]+\}))/', $offset, $length, $index, $name )); // !
2435 if( (count($name) > 1) && $this->rulePropertyMatch('/\\G\s*((?:\+_|\+)?)\s*:/', $offset, $length, $index, $name) ){
2436 // at last, we have the complete match now. move forward,
2437 // convert name particles to tree objects and return:
2438 $this->skipWhitespace($length);
2440 if( $name[0] === '' ){
2441 array_shift($name);
2442 array_shift($index);
2444 foreach($name as $k => $s ){
2445 if( !$s || $s[0] !== '@' ){
2446 $name[$k] = $this->NewObj1('Less_Tree_Keyword',$s);
2447 }else{
2448 $name[$k] = $this->NewObj3('Less_Tree_Variable',array('@' . substr($s,2,-1), $index[$k], $this->env->currentFileInfo));
2451 return $name;
2457 private function rulePropertyMatch( $re, &$offset, &$length, &$index, &$name ){
2458 preg_match($re, $this->input, $a, 0, $offset);
2459 if( $a ){
2460 $index[] = $this->pos + $length;
2461 $length += strlen($a[0]);
2462 $offset += strlen($a[0]);
2463 $name[] = $a[1];
2464 return true;
2468 public static function serializeVars( $vars ){
2469 $s = '';
2471 foreach($vars as $name => $value){
2472 $s .= (($name[0] === '@') ? '' : '@') . $name .': '. $value . ((substr($value,-1) === ';') ? '' : ';');
2475 return $s;
2480 * Some versions of php have trouble with method_exists($a,$b) if $a is not an object
2482 * @param string $b
2484 public static function is_method($a,$b){
2485 return is_object($a) && method_exists($a,$b);
2490 * Round numbers similarly to javascript
2491 * eg: 1.499999 to 1 instead of 2
2494 public static function round($i, $precision = 0){
2496 $precision = pow(10,$precision);
2497 $i = $i*$precision;
2499 $ceil = ceil($i);
2500 $floor = floor($i);
2501 if( ($ceil - $i) <= ($i - $floor) ){
2502 return $ceil/$precision;
2503 }else{
2504 return $floor/$precision;
2510 * Create Less_Tree_* objects and optionally generate a cache string
2512 * @return mixed
2514 public function NewObj0($class){
2515 $obj = new $class();
2516 if( $this->CacheEnabled() ){
2517 $obj->cache_string = ' new '.$class.'()';
2519 return $obj;
2522 public function NewObj1($class, $arg){
2523 $obj = new $class( $arg );
2524 if( $this->CacheEnabled() ){
2525 $obj->cache_string = ' new '.$class.'('.Less_Parser::ArgString($arg).')';
2527 return $obj;
2530 public function NewObj2($class, $args){
2531 $obj = new $class( $args[0], $args[1] );
2532 if( $this->CacheEnabled() ){
2533 $this->ObjCache( $obj, $class, $args);
2535 return $obj;
2538 public function NewObj3($class, $args){
2539 $obj = new $class( $args[0], $args[1], $args[2] );
2540 if( $this->CacheEnabled() ){
2541 $this->ObjCache( $obj, $class, $args);
2543 return $obj;
2546 public function NewObj4($class, $args){
2547 $obj = new $class( $args[0], $args[1], $args[2], $args[3] );
2548 if( $this->CacheEnabled() ){
2549 $this->ObjCache( $obj, $class, $args);
2551 return $obj;
2554 public function NewObj5($class, $args){
2555 $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4] );
2556 if( $this->CacheEnabled() ){
2557 $this->ObjCache( $obj, $class, $args);
2559 return $obj;
2562 public function NewObj6($class, $args){
2563 $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5] );
2564 if( $this->CacheEnabled() ){
2565 $this->ObjCache( $obj, $class, $args);
2567 return $obj;
2570 public function NewObj7($class, $args){
2571 $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6] );
2572 if( $this->CacheEnabled() ){
2573 $this->ObjCache( $obj, $class, $args);
2575 return $obj;
2578 //caching
2579 public function ObjCache($obj, $class, $args=array()){
2580 $obj->cache_string = ' new '.$class.'('. self::ArgCache($args).')';
2583 public function ArgCache($args){
2584 return implode(',',array_map( array('Less_Parser','ArgString'),$args));
2589 * Convert an argument to a string for use in the parser cache
2591 * @return string
2593 public static function ArgString($arg){
2595 $type = gettype($arg);
2597 if( $type === 'object'){
2598 $string = $arg->cache_string;
2599 unset($arg->cache_string);
2600 return $string;
2602 }elseif( $type === 'array' ){
2603 $string = ' Array(';
2604 foreach($arg as $k => $a){
2605 $string .= var_export($k,true).' => '.self::ArgString($a).',';
2607 return $string . ')';
2610 return var_export($arg,true);
2613 public function Error($msg){
2614 throw new Less_Exception_Parser($msg, null, $this->furthest, $this->env->currentFileInfo);
2617 public static function WinPath($path){
2618 return str_replace('\\', '/', $path);
2621 public function CacheEnabled(){
2622 return (Less_Parser::$options['cache_method'] && (Less_Cache::$cache_dir || (Less_Parser::$options['cache_method'] == 'callback')));