Merge branch 'MDL-61960-master' of git://github.com/farhan6318/moodle
[moodle.git] / lib / lessphp / Parser.php
blob3871eb42dba9fb5eee2eec58fc8350f3abeb8b14
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' => '', // 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 'indentation' => ' ',
40 'plugins' => array(),
44 public static $options = array();
47 private $input; // Less input string
48 private $input_len; // input string length
49 private $pos; // current index in `input`
50 private $saveStack = array(); // holds state for backtracking
51 private $furthest;
52 private $mb_internal_encoding = ''; // for remember exists value of mbstring.internal_encoding
54 /**
55 * @var Less_Environment
57 private $env;
59 protected $rules = array();
61 private static $imports = array();
63 public static $has_extends = false;
65 public static $next_id = 0;
67 /**
68 * Filename to contents of all parsed the files
70 * @var array
72 public static $contentsMap = array();
75 /**
76 * @param Less_Environment|array|null $env
78 public function __construct( $env = null ){
80 // Top parser on an import tree must be sure there is one "env"
81 // which will then be passed around by reference.
82 if( $env instanceof Less_Environment ){
83 $this->env = $env;
84 }else{
85 $this->SetOptions(Less_Parser::$default_options);
86 $this->Reset( $env );
89 // mbstring.func_overload > 1 bugfix
90 // The encoding value must be set for each source file,
91 // therefore, to conserve resources and improve the speed of this design is taken here
92 if (ini_get('mbstring.func_overload')) {
93 $this->mb_internal_encoding = ini_get('mbstring.internal_encoding');
94 @ini_set('mbstring.internal_encoding', 'ascii');
101 * Reset the parser state completely
104 public function Reset( $options = null ){
105 $this->rules = array();
106 self::$imports = array();
107 self::$has_extends = false;
108 self::$imports = array();
109 self::$contentsMap = array();
111 $this->env = new Less_Environment($options);
112 $this->env->Init();
114 //set new options
115 if( is_array($options) ){
116 $this->SetOptions(Less_Parser::$default_options);
117 $this->SetOptions($options);
122 * Set one or more compiler options
123 * options: import_dirs, cache_dir, cache_method
126 public function SetOptions( $options ){
127 foreach($options as $option => $value){
128 $this->SetOption($option,$value);
133 * Set one compiler option
136 public function SetOption($option,$value){
138 switch($option){
140 case 'import_dirs':
141 $this->SetImportDirs($value);
142 return;
144 case 'cache_dir':
145 if( is_string($value) ){
146 Less_Cache::SetCacheDir($value);
147 Less_Cache::CheckCacheDir();
149 return;
152 Less_Parser::$options[$option] = $value;
156 * Registers a new custom function
158 * @param string $name function name
159 * @param callable $callback callback
161 public function registerFunction($name, $callback) {
162 $this->env->functions[$name] = $callback;
166 * Removed an already registered function
168 * @param string $name function name
170 public function unregisterFunction($name) {
171 if( isset($this->env->functions[$name]) )
172 unset($this->env->functions[$name]);
177 * Get the current css buffer
179 * @return string
181 public function getCss(){
183 $precision = ini_get('precision');
184 @ini_set('precision',16);
185 $locale = setlocale(LC_NUMERIC, 0);
186 setlocale(LC_NUMERIC, "C");
188 try {
190 $root = new Less_Tree_Ruleset(array(), $this->rules );
191 $root->root = true;
192 $root->firstRoot = true;
195 $this->PreVisitors($root);
197 self::$has_extends = false;
198 $evaldRoot = $root->compile($this->env);
202 $this->PostVisitors($evaldRoot);
204 if( Less_Parser::$options['sourceMap'] ){
205 $generator = new Less_SourceMap_Generator($evaldRoot, Less_Parser::$contentsMap, Less_Parser::$options );
206 // will also save file
207 // FIXME: should happen somewhere else?
208 $css = $generator->generateCSS();
209 }else{
210 $css = $evaldRoot->toCSS();
213 if( Less_Parser::$options['compress'] ){
214 $css = preg_replace('/(^(\s)+)|((\s)+$)/', '', $css);
217 } catch (Exception $exc) {
218 // Intentional fall-through so we can reset environment
221 //reset php settings
222 @ini_set('precision',$precision);
223 setlocale(LC_NUMERIC, $locale);
225 // If you previously defined $this->mb_internal_encoding
226 // is required to return the encoding as it was before
227 if ($this->mb_internal_encoding != '') {
228 @ini_set("mbstring.internal_encoding", $this->mb_internal_encoding);
229 $this->mb_internal_encoding = '';
232 // Rethrow exception after we handled resetting the environment
233 if (!empty($exc)) {
234 throw $exc;
239 return $css;
243 * Run pre-compile visitors
246 private function PreVisitors($root){
248 if( Less_Parser::$options['plugins'] ){
249 foreach(Less_Parser::$options['plugins'] as $plugin){
250 if( !empty($plugin->isPreEvalVisitor) ){
251 $plugin->run($root);
259 * Run post-compile visitors
262 private function PostVisitors($evaldRoot){
264 $visitors = array();
265 $visitors[] = new Less_Visitor_joinSelector();
266 if( self::$has_extends ){
267 $visitors[] = new Less_Visitor_processExtends();
269 $visitors[] = new Less_Visitor_toCSS();
272 if( Less_Parser::$options['plugins'] ){
273 foreach(Less_Parser::$options['plugins'] as $plugin){
274 if( property_exists($plugin,'isPreEvalVisitor') && $plugin->isPreEvalVisitor ){
275 continue;
278 if( property_exists($plugin,'isPreVisitor') && $plugin->isPreVisitor ){
279 array_unshift( $visitors, $plugin);
280 }else{
281 $visitors[] = $plugin;
287 for($i = 0; $i < count($visitors); $i++ ){
288 $visitors[$i]->run($evaldRoot);
295 * Parse a Less string into css
297 * @param string $str The string to convert
298 * @param string $uri_root The url of the file
299 * @return Less_Tree_Ruleset|Less_Parser
301 public function parse( $str, $file_uri = null ){
303 if( !$file_uri ){
304 $uri_root = '';
305 $filename = 'anonymous-file-'.Less_Parser::$next_id++.'.less';
306 }else{
307 $file_uri = self::WinPath($file_uri);
308 $filename = $file_uri;
309 $uri_root = dirname($file_uri);
312 $previousFileInfo = $this->env->currentFileInfo;
313 $uri_root = self::WinPath($uri_root);
314 $this->SetFileInfo($filename, $uri_root);
316 $this->input = $str;
317 $this->_parse();
319 if( $previousFileInfo ){
320 $this->env->currentFileInfo = $previousFileInfo;
323 return $this;
328 * Parse a Less string from a given file
330 * @throws Less_Exception_Parser
331 * @param string $filename The file to parse
332 * @param string $uri_root The url of the file
333 * @param bool $returnRoot Indicates whether the return value should be a css string a root node
334 * @return Less_Tree_Ruleset|Less_Parser
336 public function parseFile( $filename, $uri_root = '', $returnRoot = false){
338 if( !file_exists($filename) ){
339 $this->Error(sprintf('File `%s` not found.', $filename));
343 // fix uri_root?
344 // Instead of The mixture of file path for the first argument and directory path for the second argument has bee
345 if( !$returnRoot && !empty($uri_root) && basename($uri_root) == basename($filename) ){
346 $uri_root = dirname($uri_root);
350 $previousFileInfo = $this->env->currentFileInfo;
353 if( $filename ){
354 $filename = self::WinPath(realpath($filename));
356 $uri_root = self::WinPath($uri_root);
358 $this->SetFileInfo($filename, $uri_root);
360 self::AddParsedFile($filename);
362 if( $returnRoot ){
363 $rules = $this->GetRules( $filename );
364 $return = new Less_Tree_Ruleset(array(), $rules );
365 }else{
366 $this->_parse( $filename );
367 $return = $this;
370 if( $previousFileInfo ){
371 $this->env->currentFileInfo = $previousFileInfo;
374 return $return;
379 * Allows a user to set variables values
380 * @param array $vars
381 * @return Less_Parser
383 public function ModifyVars( $vars ){
385 $this->input = Less_Parser::serializeVars( $vars );
386 $this->_parse();
388 return $this;
393 * @param string $filename
395 public function SetFileInfo( $filename, $uri_root = ''){
397 $filename = Less_Environment::normalizePath($filename);
398 $dirname = preg_replace('/[^\/\\\\]*$/','',$filename);
400 if( !empty($uri_root) ){
401 $uri_root = rtrim($uri_root,'/').'/';
404 $currentFileInfo = array();
406 //entry info
407 if( isset($this->env->currentFileInfo) ){
408 $currentFileInfo['entryPath'] = $this->env->currentFileInfo['entryPath'];
409 $currentFileInfo['entryUri'] = $this->env->currentFileInfo['entryUri'];
410 $currentFileInfo['rootpath'] = $this->env->currentFileInfo['rootpath'];
412 }else{
413 $currentFileInfo['entryPath'] = $dirname;
414 $currentFileInfo['entryUri'] = $uri_root;
415 $currentFileInfo['rootpath'] = $dirname;
418 $currentFileInfo['currentDirectory'] = $dirname;
419 $currentFileInfo['currentUri'] = $uri_root.basename($filename);
420 $currentFileInfo['filename'] = $filename;
421 $currentFileInfo['uri_root'] = $uri_root;
424 //inherit reference
425 if( isset($this->env->currentFileInfo['reference']) && $this->env->currentFileInfo['reference'] ){
426 $currentFileInfo['reference'] = true;
429 $this->env->currentFileInfo = $currentFileInfo;
434 * @deprecated 1.5.1.2
437 public function SetCacheDir( $dir ){
439 if( !file_exists($dir) ){
440 if( mkdir($dir) ){
441 return true;
443 throw new Less_Exception_Parser('Less.php cache directory couldn\'t be created: '.$dir);
445 }elseif( !is_dir($dir) ){
446 throw new Less_Exception_Parser('Less.php cache directory doesn\'t exist: '.$dir);
448 }elseif( !is_writable($dir) ){
449 throw new Less_Exception_Parser('Less.php cache directory isn\'t writable: '.$dir);
451 }else{
452 $dir = self::WinPath($dir);
453 Less_Cache::$cache_dir = rtrim($dir,'/').'/';
454 return true;
460 * Set a list of directories or callbacks the parser should use for determining import paths
462 * @param array $dirs
464 public function SetImportDirs( $dirs ){
465 Less_Parser::$options['import_dirs'] = array();
467 foreach($dirs as $path => $uri_root){
469 $path = self::WinPath($path);
470 if( !empty($path) ){
471 $path = rtrim($path,'/').'/';
474 if ( !is_callable($uri_root) ){
475 $uri_root = self::WinPath($uri_root);
476 if( !empty($uri_root) ){
477 $uri_root = rtrim($uri_root,'/').'/';
481 Less_Parser::$options['import_dirs'][$path] = $uri_root;
486 * @param string $file_path
488 private function _parse( $file_path = null ){
489 $this->rules = array_merge($this->rules, $this->GetRules( $file_path ));
494 * Return the results of parsePrimary for $file_path
495 * Use cache and save cached results if possible
497 * @param string|null $file_path
499 private function GetRules( $file_path ){
501 $this->SetInput($file_path);
503 $cache_file = $this->CacheFile( $file_path );
504 if( $cache_file ){
505 if( Less_Parser::$options['cache_method'] == 'callback' ){
506 if( is_callable(Less_Parser::$options['cache_callback_get']) ){
507 $cache = call_user_func_array(
508 Less_Parser::$options['cache_callback_get'],
509 array($this, $file_path, $cache_file)
512 if( $cache ){
513 $this->UnsetInput();
514 return $cache;
518 }elseif( file_exists($cache_file) ){
519 switch(Less_Parser::$options['cache_method']){
521 // Using serialize
522 // Faster but uses more memory
523 case 'serialize':
524 $cache = unserialize(file_get_contents($cache_file));
525 if( $cache ){
526 touch($cache_file);
527 $this->UnsetInput();
528 return $cache;
530 break;
533 // Using generated php code
534 case 'var_export':
535 case 'php':
536 $this->UnsetInput();
537 return include($cache_file);
542 $rules = $this->parsePrimary();
544 if( $this->pos < $this->input_len ){
545 throw new Less_Exception_Chunk($this->input, null, $this->furthest, $this->env->currentFileInfo);
548 $this->UnsetInput();
551 //save the cache
552 if( $cache_file ){
553 if( Less_Parser::$options['cache_method'] == 'callback' ){
554 if( is_callable(Less_Parser::$options['cache_callback_set']) ){
555 call_user_func_array(
556 Less_Parser::$options['cache_callback_set'],
557 array($this, $file_path, $cache_file, $rules)
561 }else{
562 //msg('write cache file');
563 switch(Less_Parser::$options['cache_method']){
564 case 'serialize':
565 file_put_contents( $cache_file, serialize($rules) );
566 break;
567 case 'php':
568 file_put_contents( $cache_file, '<?php return '.self::ArgString($rules).'; ?>' );
569 break;
570 case 'var_export':
571 //Requires __set_state()
572 file_put_contents( $cache_file, '<?php return '.var_export($rules,true).'; ?>' );
573 break;
576 Less_Cache::CleanCache();
580 return $rules;
585 * Set up the input buffer
588 public function SetInput( $file_path ){
590 if( $file_path ){
591 $this->input = file_get_contents( $file_path );
594 $this->pos = $this->furthest = 0;
596 // Remove potential UTF Byte Order Mark
597 $this->input = preg_replace('/\\G\xEF\xBB\xBF/', '', $this->input);
598 $this->input_len = strlen($this->input);
601 if( Less_Parser::$options['sourceMap'] && $this->env->currentFileInfo ){
602 $uri = $this->env->currentFileInfo['currentUri'];
603 Less_Parser::$contentsMap[$uri] = $this->input;
610 * Free up some memory
613 public function UnsetInput(){
614 unset($this->input, $this->pos, $this->input_len, $this->furthest);
615 $this->saveStack = array();
619 public function CacheFile( $file_path ){
621 if( $file_path && $this->CacheEnabled() ){
623 $env = get_object_vars($this->env);
624 unset($env['frames']);
626 $parts = array();
627 $parts[] = $file_path;
628 $parts[] = filesize( $file_path );
629 $parts[] = filemtime( $file_path );
630 $parts[] = $env;
631 $parts[] = Less_Version::cache_version;
632 $parts[] = Less_Parser::$options['cache_method'];
633 return Less_Cache::$cache_dir . Less_Cache::$prefix . base_convert( sha1(json_encode($parts) ), 16, 36) . '.lesscache';
638 static function AddParsedFile($file){
639 self::$imports[] = $file;
642 static function AllParsedFiles(){
643 return self::$imports;
647 * @param string $file
649 static function FileParsed($file){
650 return in_array($file,self::$imports);
654 function save() {
655 $this->saveStack[] = $this->pos;
658 private function restore() {
659 $this->pos = array_pop($this->saveStack);
662 private function forget(){
663 array_pop($this->saveStack);
667 private function isWhitespace($offset = 0) {
668 return preg_match('/\s/',$this->input[ $this->pos + $offset]);
672 * Parse from a token, regexp or string, and move forward if match
674 * @param array $toks
675 * @return array
677 private function match($toks){
679 // The match is confirmed, add the match length to `this::pos`,
680 // and consume any extra white-space characters (' ' || '\n')
681 // which come after that. The reason for this is that LeSS's
682 // grammar is mostly white-space insensitive.
685 foreach($toks as $tok){
687 $char = $tok[0];
689 if( $char === '/' ){
690 $match = $this->MatchReg($tok);
692 if( $match ){
693 return count($match) === 1 ? $match[0] : $match;
696 }elseif( $char === '#' ){
697 $match = $this->MatchChar($tok[1]);
699 }else{
700 // Non-terminal, match using a function call
701 $match = $this->$tok();
705 if( $match ){
706 return $match;
712 * @param string[] $toks
714 * @return string
716 private function MatchFuncs($toks){
718 if( $this->pos < $this->input_len ){
719 foreach($toks as $tok){
720 $match = $this->$tok();
721 if( $match ){
722 return $match;
729 // Match a single character in the input,
730 private function MatchChar($tok){
731 if( ($this->pos < $this->input_len) && ($this->input[$this->pos] === $tok) ){
732 $this->skipWhitespace(1);
733 return $tok;
737 // Match a regexp from the current start point
738 private function MatchReg($tok){
740 if( preg_match($tok, $this->input, $match, 0, $this->pos) ){
741 $this->skipWhitespace(strlen($match[0]));
742 return $match;
748 * Same as match(), but don't change the state of the parser,
749 * just return the match.
751 * @param string $tok
752 * @return integer
754 public function PeekReg($tok){
755 return preg_match($tok, $this->input, $match, 0, $this->pos);
759 * @param string $tok
761 public function PeekChar($tok){
762 //return ($this->input[$this->pos] === $tok );
763 return ($this->pos < $this->input_len) && ($this->input[$this->pos] === $tok );
768 * @param integer $length
770 public function skipWhitespace($length){
772 $this->pos += $length;
774 for(; $this->pos < $this->input_len; $this->pos++ ){
775 $c = $this->input[$this->pos];
777 if( ($c !== "\n") && ($c !== "\r") && ($c !== "\t") && ($c !== ' ') ){
778 break;
785 * @param string $tok
786 * @param string|null $msg
788 public function expect($tok, $msg = NULL) {
789 $result = $this->match( array($tok) );
790 if (!$result) {
791 $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg );
792 } else {
793 return $result;
798 * @param string $tok
800 public function expectChar($tok, $msg = null ){
801 $result = $this->MatchChar($tok);
802 if( !$result ){
803 $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg );
804 }else{
805 return $result;
810 // Here in, the parsing rules/functions
812 // The basic structure of the syntax tree generated is as follows:
814 // Ruleset -> Rule -> Value -> Expression -> Entity
816 // Here's some LESS code:
818 // .class {
819 // color: #fff;
820 // border: 1px solid #000;
821 // width: @w + 4px;
822 // > .child {...}
823 // }
825 // And here's what the parse tree might look like:
827 // Ruleset (Selector '.class', [
828 // Rule ("color", Value ([Expression [Color #fff]]))
829 // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]]))
830 // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]]))
831 // Ruleset (Selector [Element '>', '.child'], [...])
832 // ])
834 // In general, most rules will try to parse a token with the `$()` function, and if the return
835 // value is truly, will return a new node, of the relevant type. Sometimes, we need to check
836 // first, before parsing, that's when we use `peek()`.
840 // The `primary` rule is the *entry* and *exit* point of the parser.
841 // The rules here can appear at any level of the parse tree.
843 // The recursive nature of the grammar is an interplay between the `block`
844 // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,
845 // as represented by this simplified grammar:
847 // primary → (ruleset | rule)+
848 // ruleset → selector+ block
849 // block → '{' primary '}'
851 // Only at one point is the primary rule not called from the
852 // block rule: at the root level.
854 private function parsePrimary(){
855 $root = array();
857 while( true ){
859 if( $this->pos >= $this->input_len ){
860 break;
863 $node = $this->parseExtend(true);
864 if( $node ){
865 $root = array_merge($root,$node);
866 continue;
869 //$node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseDirective'));
870 $node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseNameValue', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseRulesetCall', 'parseDirective'));
872 if( $node ){
873 $root[] = $node;
874 }elseif( !$this->MatchReg('/\\G[\s\n;]+/') ){
875 break;
878 if( $this->PeekChar('}') ){
879 break;
883 return $root;
888 // We create a Comment node for CSS comments `/* */`,
889 // but keep the LeSS comments `//` silent, by just skipping
890 // over them.
891 private function parseComment(){
893 if( $this->input[$this->pos] !== '/' ){
894 return;
897 if( $this->input[$this->pos+1] === '/' ){
898 $match = $this->MatchReg('/\\G\/\/.*/');
899 return $this->NewObj4('Less_Tree_Comment',array($match[0], true, $this->pos, $this->env->currentFileInfo));
902 //$comment = $this->MatchReg('/\\G\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/');
903 $comment = $this->MatchReg('/\\G\/\*(?s).*?\*+\/\n?/');//not the same as less.js to prevent fatal errors
904 if( $comment ){
905 return $this->NewObj4('Less_Tree_Comment',array($comment[0], false, $this->pos, $this->env->currentFileInfo));
909 private function parseComments(){
910 $comments = array();
912 while( $this->pos < $this->input_len ){
913 $comment = $this->parseComment();
914 if( !$comment ){
915 break;
918 $comments[] = $comment;
921 return $comments;
927 // A string, which supports escaping " and '
929 // "milky way" 'he\'s the one!'
931 private function parseEntitiesQuoted() {
932 $j = $this->pos;
933 $e = false;
934 $index = $this->pos;
936 if( $this->input[$this->pos] === '~' ){
937 $j++;
938 $e = true; // Escaped strings
941 if( $this->input[$j] != '"' && $this->input[$j] !== "'" ){
942 return;
945 if ($e) {
946 $this->MatchChar('~');
949 // Fix for #124: match escaped newlines
950 //$str = $this->MatchReg('/\\G"((?:[^"\\\\\r\n]|\\\\.)*)"|\'((?:[^\'\\\\\r\n]|\\\\.)*)\'/');
951 $str = $this->MatchReg('/\\G"((?:[^"\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)"|\'((?:[^\'\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)\'/');
953 if( $str ){
954 $result = $str[0][0] == '"' ? $str[1] : $str[2];
955 return $this->NewObj5('Less_Tree_Quoted',array($str[0], $result, $e, $index, $this->env->currentFileInfo) );
957 return;
962 // A catch-all word, such as:
964 // black border-collapse
966 private function parseEntitiesKeyword(){
968 //$k = $this->MatchReg('/\\G[_A-Za-z-][_A-Za-z0-9-]*/');
969 $k = $this->MatchReg('/\\G%|\\G[_A-Za-z-][_A-Za-z0-9-]*/');
970 if( $k ){
971 $k = $k[0];
972 $color = $this->fromKeyword($k);
973 if( $color ){
974 return $color;
976 return $this->NewObj1('Less_Tree_Keyword',$k);
980 // duplicate of Less_Tree_Color::FromKeyword
981 private function FromKeyword( $keyword ){
982 $keyword = strtolower($keyword);
984 if( Less_Colors::hasOwnProperty($keyword) ){
985 // detect named color
986 return $this->NewObj1('Less_Tree_Color',substr(Less_Colors::color($keyword), 1));
989 if( $keyword === 'transparent' ){
990 return $this->NewObj3('Less_Tree_Color', array( array(0, 0, 0), 0, true));
995 // A function call
997 // rgb(255, 0, 255)
999 // We also try to catch IE's `alpha()`, but let the `alpha` parser
1000 // deal with the details.
1002 // The arguments are parsed with the `entities.arguments` parser.
1004 private function parseEntitiesCall(){
1005 $index = $this->pos;
1007 if( !preg_match('/\\G([\w-]+|%|progid:[\w\.]+)\(/', $this->input, $name,0,$this->pos) ){
1008 return;
1010 $name = $name[1];
1011 $nameLC = strtolower($name);
1013 if ($nameLC === 'url') {
1014 return null;
1017 $this->pos += strlen($name);
1019 if( $nameLC === 'alpha' ){
1020 $alpha_ret = $this->parseAlpha();
1021 if( $alpha_ret ){
1022 return $alpha_ret;
1026 $this->MatchChar('('); // Parse the '(' and consume whitespace.
1028 $args = $this->parseEntitiesArguments();
1030 if( !$this->MatchChar(')') ){
1031 return;
1034 if ($name) {
1035 return $this->NewObj4('Less_Tree_Call',array($name, $args, $index, $this->env->currentFileInfo) );
1040 * Parse a list of arguments
1042 * @return array
1044 private function parseEntitiesArguments(){
1046 $args = array();
1047 while( true ){
1048 $arg = $this->MatchFuncs( array('parseEntitiesAssignment','parseExpression') );
1049 if( !$arg ){
1050 break;
1053 $args[] = $arg;
1054 if( !$this->MatchChar(',') ){
1055 break;
1058 return $args;
1061 private function parseEntitiesLiteral(){
1062 return $this->MatchFuncs( array('parseEntitiesDimension','parseEntitiesColor','parseEntitiesQuoted','parseUnicodeDescriptor') );
1065 // Assignments are argument entities for calls.
1066 // They are present in ie filter properties as shown below.
1068 // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* )
1070 private function parseEntitiesAssignment() {
1072 $key = $this->MatchReg('/\\G\w+(?=\s?=)/');
1073 if( !$key ){
1074 return;
1077 if( !$this->MatchChar('=') ){
1078 return;
1081 $value = $this->parseEntity();
1082 if( $value ){
1083 return $this->NewObj2('Less_Tree_Assignment',array($key[0], $value));
1088 // Parse url() tokens
1090 // We use a specific rule for urls, because they don't really behave like
1091 // standard function calls. The difference is that the argument doesn't have
1092 // to be enclosed within a string, so it can't be parsed as an Expression.
1094 private function parseEntitiesUrl(){
1097 if( $this->input[$this->pos] !== 'u' || !$this->matchReg('/\\Gurl\(/') ){
1098 return;
1101 $value = $this->match( array('parseEntitiesQuoted','parseEntitiesVariable','/\\Gdata\:.*?[^\)]+/','/\\G(?:(?:\\\\[\(\)\'"])|[^\(\)\'"])+/') );
1102 if( !$value ){
1103 $value = '';
1107 $this->expectChar(')');
1110 if( isset($value->value) || $value instanceof Less_Tree_Variable ){
1111 return $this->NewObj2('Less_Tree_Url',array($value, $this->env->currentFileInfo));
1114 return $this->NewObj2('Less_Tree_Url', array( $this->NewObj1('Less_Tree_Anonymous',$value), $this->env->currentFileInfo) );
1119 // A Variable entity, such as `@fink`, in
1121 // width: @fink + 2px
1123 // We use a different parser for variable definitions,
1124 // see `parsers.variable`.
1126 private function parseEntitiesVariable(){
1127 $index = $this->pos;
1128 if ($this->PeekChar('@') && ($name = $this->MatchReg('/\\G@@?[\w-]+/'))) {
1129 return $this->NewObj3('Less_Tree_Variable', array( $name[0], $index, $this->env->currentFileInfo));
1134 // A variable entity useing the protective {} e.g. @{var}
1135 private function parseEntitiesVariableCurly() {
1136 $index = $this->pos;
1138 if( $this->input_len > ($this->pos+1) && $this->input[$this->pos] === '@' && ($curly = $this->MatchReg('/\\G@\{([\w-]+)\}/')) ){
1139 return $this->NewObj3('Less_Tree_Variable',array('@'.$curly[1], $index, $this->env->currentFileInfo));
1144 // A Hexadecimal color
1146 // #4F3C2F
1148 // `rgb` and `hsl` colors are parsed through the `entities.call` parser.
1150 private function parseEntitiesColor(){
1151 if ($this->PeekChar('#') && ($rgb = $this->MatchReg('/\\G#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/'))) {
1152 return $this->NewObj1('Less_Tree_Color',$rgb[1]);
1157 // A Dimension, that is, a number and a unit
1159 // 0.5em 95%
1161 private function parseEntitiesDimension(){
1163 $c = @ord($this->input[$this->pos]);
1165 //Is the first char of the dimension 0-9, '.', '+' or '-'
1166 if (($c > 57 || $c < 43) || $c === 47 || $c == 44){
1167 return;
1170 $value = $this->MatchReg('/\\G([+-]?\d*\.?\d+)(%|[a-z]+)?/');
1171 if( $value ){
1173 if( isset($value[2]) ){
1174 return $this->NewObj2('Less_Tree_Dimension', array($value[1],$value[2]));
1176 return $this->NewObj1('Less_Tree_Dimension',$value[1]);
1182 // A unicode descriptor, as is used in unicode-range
1184 // U+0?? or U+00A1-00A9
1186 function parseUnicodeDescriptor() {
1187 $ud = $this->MatchReg('/\\G(U\+[0-9a-fA-F?]+)(\-[0-9a-fA-F?]+)?/');
1188 if( $ud ){
1189 return $this->NewObj1('Less_Tree_UnicodeDescriptor', $ud[0]);
1195 // JavaScript code to be evaluated
1197 // `window.location.href`
1199 private function parseEntitiesJavascript(){
1200 $e = false;
1201 $j = $this->pos;
1202 if( $this->input[$j] === '~' ){
1203 $j++;
1204 $e = true;
1206 if( $this->input[$j] !== '`' ){
1207 return;
1209 if( $e ){
1210 $this->MatchChar('~');
1212 $str = $this->MatchReg('/\\G`([^`]*)`/');
1213 if( $str ){
1214 return $this->NewObj3('Less_Tree_Javascript', array($str[1], $this->pos, $e));
1220 // The variable part of a variable definition. Used in the `rule` parser
1222 // @fink:
1224 private function parseVariable(){
1225 if ($this->PeekChar('@') && ($name = $this->MatchReg('/\\G(@[\w-]+)\s*:/'))) {
1226 return $name[1];
1232 // The variable part of a variable definition. Used in the `rule` parser
1234 // @fink();
1236 private function parseRulesetCall(){
1238 if( $this->input[$this->pos] === '@' && ($name = $this->MatchReg('/\\G(@[\w-]+)\s*\(\s*\)\s*;/')) ){
1239 return $this->NewObj1('Less_Tree_RulesetCall', $name[1] );
1245 // extend syntax - used to extend selectors
1247 function parseExtend($isRule = false){
1249 $index = $this->pos;
1250 $extendList = array();
1253 if( !$this->MatchReg( $isRule ? '/\\G&:extend\(/' : '/\\G:extend\(/' ) ){ return; }
1256 $option = null;
1257 $elements = array();
1258 while( true ){
1259 $option = $this->MatchReg('/\\G(all)(?=\s*(\)|,))/');
1260 if( $option ){ break; }
1261 $e = $this->parseElement();
1262 if( !$e ){ break; }
1263 $elements[] = $e;
1266 if( $option ){
1267 $option = $option[1];
1270 $extendList[] = $this->NewObj3('Less_Tree_Extend', array( $this->NewObj1('Less_Tree_Selector',$elements), $option, $index ));
1272 }while( $this->MatchChar(",") );
1274 $this->expect('/\\G\)/');
1276 if( $isRule ){
1277 $this->expect('/\\G;/');
1280 return $extendList;
1285 // A Mixin call, with an optional argument list
1287 // #mixins > .square(#fff);
1288 // .rounded(4px, black);
1289 // .button;
1291 // The `while` loop is there because mixins can be
1292 // namespaced, but we only support the child and descendant
1293 // selector for now.
1295 private function parseMixinCall(){
1297 $char = $this->input[$this->pos];
1298 if( $char !== '.' && $char !== '#' ){
1299 return;
1302 $index = $this->pos;
1303 $this->save(); // stop us absorbing part of an invalid selector
1305 $elements = $this->parseMixinCallElements();
1307 if( $elements ){
1309 if( $this->MatchChar('(') ){
1310 $returned = $this->parseMixinArgs(true);
1311 $args = $returned['args'];
1312 $this->expectChar(')');
1313 }else{
1314 $args = array();
1317 $important = $this->parseImportant();
1319 if( $this->parseEnd() ){
1320 $this->forget();
1321 return $this->NewObj5('Less_Tree_Mixin_Call', array( $elements, $args, $index, $this->env->currentFileInfo, $important));
1325 $this->restore();
1329 private function parseMixinCallElements(){
1330 $elements = array();
1331 $c = null;
1333 while( true ){
1334 $elemIndex = $this->pos;
1335 $e = $this->MatchReg('/\\G[#.](?:[\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/');
1336 if( !$e ){
1337 break;
1339 $elements[] = $this->NewObj4('Less_Tree_Element', array($c, $e[0], $elemIndex, $this->env->currentFileInfo));
1340 $c = $this->MatchChar('>');
1343 return $elements;
1349 * @param boolean $isCall
1351 private function parseMixinArgs( $isCall ){
1352 $expressions = array();
1353 $argsSemiColon = array();
1354 $isSemiColonSeperated = null;
1355 $argsComma = array();
1356 $expressionContainsNamed = null;
1357 $name = null;
1358 $returner = array('args'=>array(), 'variadic'=> false);
1360 $this->save();
1362 while( true ){
1363 if( $isCall ){
1364 $arg = $this->MatchFuncs( array( 'parseDetachedRuleset','parseExpression' ) );
1365 } else {
1366 $this->parseComments();
1367 if( $this->input[ $this->pos ] === '.' && $this->MatchReg('/\\G\.{3}/') ){
1368 $returner['variadic'] = true;
1369 if( $this->MatchChar(";") && !$isSemiColonSeperated ){
1370 $isSemiColonSeperated = true;
1373 if( $isSemiColonSeperated ){
1374 $argsSemiColon[] = array('variadic'=>true);
1375 }else{
1376 $argsComma[] = array('variadic'=>true);
1378 break;
1380 $arg = $this->MatchFuncs( array('parseEntitiesVariable','parseEntitiesLiteral','parseEntitiesKeyword') );
1383 if( !$arg ){
1384 break;
1388 $nameLoop = null;
1389 if( $arg instanceof Less_Tree_Expression ){
1390 $arg->throwAwayComments();
1392 $value = $arg;
1393 $val = null;
1395 if( $isCall ){
1396 // Variable
1397 if( property_exists($arg,'value') && count($arg->value) == 1 ){
1398 $val = $arg->value[0];
1400 } else {
1401 $val = $arg;
1405 if( $val instanceof Less_Tree_Variable ){
1407 if( $this->MatchChar(':') ){
1408 if( $expressions ){
1409 if( $isSemiColonSeperated ){
1410 $this->Error('Cannot mix ; and , as delimiter types');
1412 $expressionContainsNamed = true;
1415 // we do not support setting a ruleset as a default variable - it doesn't make sense
1416 // However if we do want to add it, there is nothing blocking it, just don't error
1417 // and remove isCall dependency below
1418 $value = null;
1419 if( $isCall ){
1420 $value = $this->parseDetachedRuleset();
1422 if( !$value ){
1423 $value = $this->parseExpression();
1426 if( !$value ){
1427 if( $isCall ){
1428 $this->Error('could not understand value for named argument');
1429 } else {
1430 $this->restore();
1431 $returner['args'] = array();
1432 return $returner;
1436 $nameLoop = ($name = $val->name);
1437 }elseif( !$isCall && $this->MatchReg('/\\G\.{3}/') ){
1438 $returner['variadic'] = true;
1439 if( $this->MatchChar(";") && !$isSemiColonSeperated ){
1440 $isSemiColonSeperated = true;
1442 if( $isSemiColonSeperated ){
1443 $argsSemiColon[] = array('name'=> $arg->name, 'variadic' => true);
1444 }else{
1445 $argsComma[] = array('name'=> $arg->name, 'variadic' => true);
1447 break;
1448 }elseif( !$isCall ){
1449 $name = $nameLoop = $val->name;
1450 $value = null;
1454 if( $value ){
1455 $expressions[] = $value;
1458 $argsComma[] = array('name'=>$nameLoop, 'value'=>$value );
1460 if( $this->MatchChar(',') ){
1461 continue;
1464 if( $this->MatchChar(';') || $isSemiColonSeperated ){
1466 if( $expressionContainsNamed ){
1467 $this->Error('Cannot mix ; and , as delimiter types');
1470 $isSemiColonSeperated = true;
1472 if( count($expressions) > 1 ){
1473 $value = $this->NewObj1('Less_Tree_Value', $expressions);
1475 $argsSemiColon[] = array('name'=>$name, 'value'=>$value );
1477 $name = null;
1478 $expressions = array();
1479 $expressionContainsNamed = false;
1483 $this->forget();
1484 $returner['args'] = ($isSemiColonSeperated ? $argsSemiColon : $argsComma);
1485 return $returner;
1491 // A Mixin definition, with a list of parameters
1493 // .rounded (@radius: 2px, @color) {
1494 // ...
1495 // }
1497 // Until we have a finer grained state-machine, we have to
1498 // do a look-ahead, to make sure we don't have a mixin call.
1499 // See the `rule` function for more information.
1501 // We start by matching `.rounded (`, and then proceed on to
1502 // the argument list, which has optional default values.
1503 // We store the parameters in `params`, with a `value` key,
1504 // if there is a value, such as in the case of `@radius`.
1506 // Once we've got our params list, and a closing `)`, we parse
1507 // the `{...}` block.
1509 private function parseMixinDefinition(){
1510 $cond = null;
1512 $char = $this->input[$this->pos];
1513 if( ($char !== '.' && $char !== '#') || ($char === '{' && $this->PeekReg('/\\G[^{]*\}/')) ){
1514 return;
1517 $this->save();
1519 $match = $this->MatchReg('/\\G([#.](?:[\w-]|\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/');
1520 if( $match ){
1521 $name = $match[1];
1523 $argInfo = $this->parseMixinArgs( false );
1524 $params = $argInfo['args'];
1525 $variadic = $argInfo['variadic'];
1528 // .mixincall("@{a}");
1529 // looks a bit like a mixin definition..
1530 // also
1531 // .mixincall(@a: {rule: set;});
1532 // so we have to be nice and restore
1533 if( !$this->MatchChar(')') ){
1534 $this->furthest = $this->pos;
1535 $this->restore();
1536 return;
1540 $this->parseComments();
1542 if ($this->MatchReg('/\\Gwhen/')) { // Guard
1543 $cond = $this->expect('parseConditions', 'Expected conditions');
1546 $ruleset = $this->parseBlock();
1548 if( is_array($ruleset) ){
1549 $this->forget();
1550 return $this->NewObj5('Less_Tree_Mixin_Definition', array( $name, $params, $ruleset, $cond, $variadic));
1553 $this->restore();
1554 }else{
1555 $this->forget();
1560 // Entities are the smallest recognized token,
1561 // and can be found inside a rule's value.
1563 private function parseEntity(){
1565 return $this->MatchFuncs( array('parseEntitiesLiteral','parseEntitiesVariable','parseEntitiesUrl','parseEntitiesCall','parseEntitiesKeyword','parseEntitiesJavascript','parseComment') );
1569 // A Rule terminator. Note that we use `peek()` to check for '}',
1570 // because the `block` rule will be expecting it, but we still need to make sure
1571 // it's there, if ';' was ommitted.
1573 private function parseEnd(){
1574 return $this->MatchChar(';') || $this->PeekChar('}');
1578 // IE's alpha function
1580 // alpha(opacity=88)
1582 private function parseAlpha(){
1584 if ( ! $this->MatchReg('/\\G\(opacity=/i')) {
1585 return;
1588 $value = $this->MatchReg('/\\G[0-9]+/');
1589 if( $value ){
1590 $value = $value[0];
1591 }else{
1592 $value = $this->parseEntitiesVariable();
1593 if( !$value ){
1594 return;
1598 $this->expectChar(')');
1599 return $this->NewObj1('Less_Tree_Alpha',$value);
1604 // A Selector Element
1606 // div
1607 // + h1
1608 // #socks
1609 // input[type="text"]
1611 // Elements are the building blocks for Selectors,
1612 // they are made out of a `Combinator` (see combinator rule),
1613 // and an element name, such as a tag a class, or `*`.
1615 private function parseElement(){
1616 $c = $this->parseCombinator();
1617 $index = $this->pos;
1619 $e = $this->match( array('/\\G(?:\d+\.\d+|\d+)%/', '/\\G(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/',
1620 '#*', '#&', 'parseAttribute', '/\\G\([^()@]+\)/', '/\\G[\.#](?=@)/', 'parseEntitiesVariableCurly') );
1622 if( is_null($e) ){
1623 $this->save();
1624 if( $this->MatchChar('(') ){
1625 if( ($v = $this->parseSelector()) && $this->MatchChar(')') ){
1626 $e = $this->NewObj1('Less_Tree_Paren',$v);
1627 $this->forget();
1628 }else{
1629 $this->restore();
1631 }else{
1632 $this->forget();
1636 if( !is_null($e) ){
1637 return $this->NewObj4('Less_Tree_Element',array( $c, $e, $index, $this->env->currentFileInfo));
1642 // Combinators combine elements together, in a Selector.
1644 // Because our parser isn't white-space sensitive, special care
1645 // has to be taken, when parsing the descendant combinator, ` `,
1646 // as it's an empty space. We have to check the previous character
1647 // in the input, to see if it's a ` ` character.
1649 private function parseCombinator(){
1650 if( $this->pos < $this->input_len ){
1651 $c = $this->input[$this->pos];
1652 if ($c === '>' || $c === '+' || $c === '~' || $c === '|' || $c === '^' ){
1654 $this->pos++;
1655 if( $this->input[$this->pos] === '^' ){
1656 $c = '^^';
1657 $this->pos++;
1660 $this->skipWhitespace(0);
1662 return $c;
1665 if( $this->pos > 0 && $this->isWhitespace(-1) ){
1666 return ' ';
1672 // A CSS selector (see selector below)
1673 // with less extensions e.g. the ability to extend and guard
1675 private function parseLessSelector(){
1676 return $this->parseSelector(true);
1680 // A CSS Selector
1682 // .class > div + h1
1683 // li a:hover
1685 // Selectors are made out of one or more Elements, see above.
1687 private function parseSelector( $isLess = false ){
1688 $elements = array();
1689 $extendList = array();
1690 $condition = null;
1691 $when = false;
1692 $extend = false;
1693 $e = null;
1694 $c = null;
1695 $index = $this->pos;
1697 while( ($isLess && ($extend = $this->parseExtend())) || ($isLess && ($when = $this->MatchReg('/\\Gwhen/') )) || ($e = $this->parseElement()) ){
1698 if( $when ){
1699 $condition = $this->expect('parseConditions', 'expected condition');
1700 }elseif( $condition ){
1701 //error("CSS guard can only be used at the end of selector");
1702 }elseif( $extend ){
1703 $extendList = array_merge($extendList,$extend);
1704 }else{
1705 //if( count($extendList) ){
1706 //error("Extend can only be used at the end of selector");
1708 if( $this->pos < $this->input_len ){
1709 $c = $this->input[ $this->pos ];
1711 $elements[] = $e;
1712 $e = null;
1715 if( $c === '{' || $c === '}' || $c === ';' || $c === ',' || $c === ')') { break; }
1718 if( $elements ){
1719 return $this->NewObj5('Less_Tree_Selector',array($elements, $extendList, $condition, $index, $this->env->currentFileInfo));
1721 if( $extendList ) {
1722 $this->Error('Extend must be used to extend a selector, it cannot be used on its own');
1726 private function parseTag(){
1727 return ( $tag = $this->MatchReg('/\\G[A-Za-z][A-Za-z-]*[0-9]?/') ) ? $tag : $this->MatchChar('*');
1730 private function parseAttribute(){
1732 $val = null;
1734 if( !$this->MatchChar('[') ){
1735 return;
1738 $key = $this->parseEntitiesVariableCurly();
1739 if( !$key ){
1740 $key = $this->expect('/\\G(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\\\.)+/');
1743 $op = $this->MatchReg('/\\G[|~*$^]?=/');
1744 if( $op ){
1745 $val = $this->match( array('parseEntitiesQuoted','/\\G[0-9]+%/','/\\G[\w-]+/','parseEntitiesVariableCurly') );
1748 $this->expectChar(']');
1750 return $this->NewObj3('Less_Tree_Attribute',array( $key, $op[0], $val));
1754 // The `block` rule is used by `ruleset` and `mixin.definition`.
1755 // It's a wrapper around the `primary` rule, with added `{}`.
1757 private function parseBlock(){
1758 if( $this->MatchChar('{') ){
1759 $content = $this->parsePrimary();
1760 if( $this->MatchChar('}') ){
1761 return $content;
1766 private function parseBlockRuleset(){
1767 $block = $this->parseBlock();
1769 if( $block ){
1770 $block = $this->NewObj2('Less_Tree_Ruleset',array( null, $block));
1773 return $block;
1776 private function parseDetachedRuleset(){
1777 $blockRuleset = $this->parseBlockRuleset();
1778 if( $blockRuleset ){
1779 return $this->NewObj1('Less_Tree_DetachedRuleset',$blockRuleset);
1784 // div, .class, body > p {...}
1786 private function parseRuleset(){
1787 $selectors = array();
1789 $this->save();
1791 while( true ){
1792 $s = $this->parseLessSelector();
1793 if( !$s ){
1794 break;
1796 $selectors[] = $s;
1797 $this->parseComments();
1799 if( $s->condition && count($selectors) > 1 ){
1800 $this->Error('Guards are only currently allowed on a single selector.');
1803 if( !$this->MatchChar(',') ){
1804 break;
1806 if( $s->condition ){
1807 $this->Error('Guards are only currently allowed on a single selector.');
1809 $this->parseComments();
1813 if( $selectors ){
1814 $rules = $this->parseBlock();
1815 if( is_array($rules) ){
1816 $this->forget();
1817 return $this->NewObj2('Less_Tree_Ruleset',array( $selectors, $rules)); //Less_Environment::$strictImports
1821 // Backtrack
1822 $this->furthest = $this->pos;
1823 $this->restore();
1827 * Custom less.php parse function for finding simple name-value css pairs
1828 * ex: width:100px;
1831 private function parseNameValue(){
1833 $index = $this->pos;
1834 $this->save();
1837 //$match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*((?:\'")?[a-zA-Z0-9\-% \.,!]+?(?:\'")?)\s*([;}])/');
1838 $match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*([\'"]?[#a-zA-Z0-9\-%\.,]+?[\'"]?) *(! *important)?\s*([;}])/');
1839 if( $match ){
1841 if( $match[4] == '}' ){
1842 $this->pos = $index + strlen($match[0])-1;
1845 if( $match[3] ){
1846 $match[2] .= ' !important';
1849 return $this->NewObj4('Less_Tree_NameValue',array( $match[1], $match[2], $index, $this->env->currentFileInfo));
1852 $this->restore();
1856 private function parseRule( $tryAnonymous = null ){
1858 $merge = false;
1859 $startOfRule = $this->pos;
1861 $c = $this->input[$this->pos];
1862 if( $c === '.' || $c === '#' || $c === '&' ){
1863 return;
1866 $this->save();
1867 $name = $this->MatchFuncs( array('parseVariable','parseRuleProperty'));
1869 if( $name ){
1871 $isVariable = is_string($name);
1873 $value = null;
1874 if( $isVariable ){
1875 $value = $this->parseDetachedRuleset();
1878 $important = null;
1879 if( !$value ){
1881 // prefer to try to parse first if its a variable or we are compressing
1882 // but always fallback on the other one
1883 //if( !$tryAnonymous && is_string($name) && $name[0] === '@' ){
1884 if( !$tryAnonymous && (Less_Parser::$options['compress'] || $isVariable) ){
1885 $value = $this->MatchFuncs( array('parseValue','parseAnonymousValue'));
1886 }else{
1887 $value = $this->MatchFuncs( array('parseAnonymousValue','parseValue'));
1890 $important = $this->parseImportant();
1892 // a name returned by this.ruleProperty() is always an array of the form:
1893 // [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"]
1894 // where each item is a tree.Keyword or tree.Variable
1895 if( !$isVariable && is_array($name) ){
1896 $nm = array_pop($name);
1897 if( $nm->value ){
1898 $merge = $nm->value;
1904 if( $value && $this->parseEnd() ){
1905 $this->forget();
1906 return $this->NewObj6('Less_Tree_Rule',array( $name, $value, $important, $merge, $startOfRule, $this->env->currentFileInfo));
1907 }else{
1908 $this->furthest = $this->pos;
1909 $this->restore();
1910 if( $value && !$tryAnonymous ){
1911 return $this->parseRule(true);
1914 }else{
1915 $this->forget();
1919 function parseAnonymousValue(){
1921 if( preg_match('/\\G([^@+\/\'"*`(;{}-]*);/',$this->input, $match, 0, $this->pos) ){
1922 $this->pos += strlen($match[1]);
1923 return $this->NewObj1('Less_Tree_Anonymous',$match[1]);
1928 // An @import directive
1930 // @import "lib";
1932 // Depending on our environment, importing is done differently:
1933 // In the browser, it's an XHR request, in Node, it would be a
1934 // file-system operation. The function used for importing is
1935 // stored in `import`, which we pass to the Import constructor.
1937 private function parseImport(){
1939 $this->save();
1941 $dir = $this->MatchReg('/\\G@import?\s+/');
1943 if( $dir ){
1944 $options = $this->parseImportOptions();
1945 $path = $this->MatchFuncs( array('parseEntitiesQuoted','parseEntitiesUrl'));
1947 if( $path ){
1948 $features = $this->parseMediaFeatures();
1949 if( $this->MatchChar(';') ){
1950 if( $features ){
1951 $features = $this->NewObj1('Less_Tree_Value',$features);
1954 $this->forget();
1955 return $this->NewObj5('Less_Tree_Import',array( $path, $features, $options, $this->pos, $this->env->currentFileInfo));
1960 $this->restore();
1963 private function parseImportOptions(){
1965 $options = array();
1967 // list of options, surrounded by parens
1968 if( !$this->MatchChar('(') ){
1969 return $options;
1972 $optionName = $this->parseImportOption();
1973 if( $optionName ){
1974 $value = true;
1975 switch( $optionName ){
1976 case "css":
1977 $optionName = "less";
1978 $value = false;
1979 break;
1980 case "once":
1981 $optionName = "multiple";
1982 $value = false;
1983 break;
1985 $options[$optionName] = $value;
1986 if( !$this->MatchChar(',') ){ break; }
1988 }while( $optionName );
1989 $this->expectChar(')');
1990 return $options;
1993 private function parseImportOption(){
1994 $opt = $this->MatchReg('/\\G(less|css|multiple|once|inline|reference|optional)/');
1995 if( $opt ){
1996 return $opt[1];
2000 private function parseMediaFeature() {
2001 $nodes = array();
2004 $e = $this->MatchFuncs(array('parseEntitiesKeyword','parseEntitiesVariable'));
2005 if( $e ){
2006 $nodes[] = $e;
2007 } elseif ($this->MatchChar('(')) {
2008 $p = $this->parseProperty();
2009 $e = $this->parseValue();
2010 if ($this->MatchChar(')')) {
2011 if ($p && $e) {
2012 $r = $this->NewObj7('Less_Tree_Rule', array( $p, $e, null, null, $this->pos, $this->env->currentFileInfo, true));
2013 $nodes[] = $this->NewObj1('Less_Tree_Paren',$r);
2014 } elseif ($e) {
2015 $nodes[] = $this->NewObj1('Less_Tree_Paren',$e);
2016 } else {
2017 return null;
2019 } else
2020 return null;
2022 } while ($e);
2024 if ($nodes) {
2025 return $this->NewObj1('Less_Tree_Expression',$nodes);
2029 private function parseMediaFeatures() {
2030 $features = array();
2033 $e = $this->parseMediaFeature();
2034 if( $e ){
2035 $features[] = $e;
2036 if (!$this->MatchChar(',')) break;
2037 }else{
2038 $e = $this->parseEntitiesVariable();
2039 if( $e ){
2040 $features[] = $e;
2041 if (!$this->MatchChar(',')) break;
2044 } while ($e);
2046 return $features ? $features : null;
2049 private function parseMedia() {
2050 if( $this->MatchReg('/\\G@media/') ){
2051 $features = $this->parseMediaFeatures();
2052 $rules = $this->parseBlock();
2054 if( is_array($rules) ){
2055 return $this->NewObj4('Less_Tree_Media',array( $rules, $features, $this->pos, $this->env->currentFileInfo));
2062 // A CSS Directive
2064 // @charset "utf-8";
2066 private function parseDirective(){
2068 if( !$this->PeekChar('@') ){
2069 return;
2072 $rules = null;
2073 $index = $this->pos;
2074 $hasBlock = true;
2075 $hasIdentifier = false;
2076 $hasExpression = false;
2077 $hasUnknown = false;
2080 $value = $this->MatchFuncs(array('parseImport','parseMedia'));
2081 if( $value ){
2082 return $value;
2085 $this->save();
2087 $name = $this->MatchReg('/\\G@[a-z-]+/');
2089 if( !$name ) return;
2090 $name = $name[0];
2093 $nonVendorSpecificName = $name;
2094 $pos = strpos($name,'-', 2);
2095 if( $name[1] == '-' && $pos > 0 ){
2096 $nonVendorSpecificName = "@" . substr($name, $pos + 1);
2100 switch( $nonVendorSpecificName ){
2102 case "@font-face":
2103 case "@viewport":
2104 case "@top-left":
2105 case "@top-left-corner":
2106 case "@top-center":
2107 case "@top-right":
2108 case "@top-right-corner":
2109 case "@bottom-left":
2110 case "@bottom-left-corner":
2111 case "@bottom-center":
2112 case "@bottom-right":
2113 case "@bottom-right-corner":
2114 case "@left-top":
2115 case "@left-middle":
2116 case "@left-bottom":
2117 case "@right-top":
2118 case "@right-middle":
2119 case "@right-bottom":
2120 hasBlock = true;
2121 break;
2123 case "@charset":
2124 $hasIdentifier = true;
2125 $hasBlock = false;
2126 break;
2127 case "@namespace":
2128 $hasExpression = true;
2129 $hasBlock = false;
2130 break;
2131 case "@keyframes":
2132 $hasIdentifier = true;
2133 break;
2134 case "@host":
2135 case "@page":
2136 case "@document":
2137 case "@supports":
2138 $hasUnknown = true;
2139 break;
2142 if( $hasIdentifier ){
2143 $value = $this->parseEntity();
2144 if( !$value ){
2145 $this->error("expected " . $name . " identifier");
2147 } else if( $hasExpression ){
2148 $value = $this->parseExpression();
2149 if( !$value ){
2150 $this->error("expected " . $name. " expression");
2152 } else if ($hasUnknown) {
2154 $value = $this->MatchReg('/\\G[^{;]+/');
2155 if( $value ){
2156 $value = $this->NewObj1('Less_Tree_Anonymous',trim($value[0]));
2160 if( $hasBlock ){
2161 $rules = $this->parseBlockRuleset();
2164 if( $rules || (!$hasBlock && $value && $this->MatchChar(';'))) {
2165 $this->forget();
2166 return $this->NewObj5('Less_Tree_Directive',array($name, $value, $rules, $index, $this->env->currentFileInfo));
2169 $this->restore();
2174 // A Value is a comma-delimited list of Expressions
2176 // font-family: Baskerville, Georgia, serif;
2178 // In a Rule, a Value represents everything after the `:`,
2179 // and before the `;`.
2181 private function parseValue(){
2182 $expressions = array();
2185 $e = $this->parseExpression();
2186 if( $e ){
2187 $expressions[] = $e;
2188 if (! $this->MatchChar(',')) {
2189 break;
2192 }while($e);
2194 if( $expressions ){
2195 return $this->NewObj1('Less_Tree_Value',$expressions);
2199 private function parseImportant (){
2200 if( $this->PeekChar('!') && $this->MatchReg('/\\G! *important/') ){
2201 return ' !important';
2205 private function parseSub (){
2207 if( $this->MatchChar('(') ){
2208 $a = $this->parseAddition();
2209 if( $a ){
2210 $this->expectChar(')');
2211 return $this->NewObj2('Less_Tree_Expression',array( array($a), true) ); //instead of $e->parens = true so the value is cached
2218 * Parses multiplication operation
2220 * @return Less_Tree_Operation|null
2222 function parseMultiplication(){
2224 $return = $m = $this->parseOperand();
2225 if( $return ){
2226 while( true ){
2228 $isSpaced = $this->isWhitespace( -1 );
2230 if( $this->PeekReg('/\\G\/[*\/]/') ){
2231 break;
2234 $op = $this->MatchChar('/');
2235 if( !$op ){
2236 $op = $this->MatchChar('*');
2237 if( !$op ){
2238 break;
2242 $a = $this->parseOperand();
2244 if(!$a) { break; }
2246 $m->parensInOp = true;
2247 $a->parensInOp = true;
2248 $return = $this->NewObj3('Less_Tree_Operation',array( $op, array( $return, $a ), $isSpaced) );
2251 return $return;
2257 * Parses an addition operation
2259 * @return Less_Tree_Operation|null
2261 private function parseAddition (){
2263 $return = $m = $this->parseMultiplication();
2264 if( $return ){
2265 while( true ){
2267 $isSpaced = $this->isWhitespace( -1 );
2269 $op = $this->MatchReg('/\\G[-+]\s+/');
2270 if( $op ){
2271 $op = $op[0];
2272 }else{
2273 if( !$isSpaced ){
2274 $op = $this->match(array('#+','#-'));
2276 if( !$op ){
2277 break;
2281 $a = $this->parseMultiplication();
2282 if( !$a ){
2283 break;
2286 $m->parensInOp = true;
2287 $a->parensInOp = true;
2288 $return = $this->NewObj3('Less_Tree_Operation',array($op, array($return, $a), $isSpaced));
2292 return $return;
2297 * Parses the conditions
2299 * @return Less_Tree_Condition|null
2301 private function parseConditions() {
2302 $index = $this->pos;
2303 $return = $a = $this->parseCondition();
2304 if( $a ){
2305 while( true ){
2306 if( !$this->PeekReg('/\\G,\s*(not\s*)?\(/') || !$this->MatchChar(',') ){
2307 break;
2309 $b = $this->parseCondition();
2310 if( !$b ){
2311 break;
2314 $return = $this->NewObj4('Less_Tree_Condition',array('or', $return, $b, $index));
2316 return $return;
2320 private function parseCondition() {
2321 $index = $this->pos;
2322 $negate = false;
2323 $c = null;
2325 if ($this->MatchReg('/\\Gnot/')) $negate = true;
2326 $this->expectChar('(');
2327 $a = $this->MatchFuncs(array('parseAddition','parseEntitiesKeyword','parseEntitiesQuoted'));
2329 if( $a ){
2330 $op = $this->MatchReg('/\\G(?:>=|<=|=<|[<=>])/');
2331 if( $op ){
2332 $b = $this->MatchFuncs(array('parseAddition','parseEntitiesKeyword','parseEntitiesQuoted'));
2333 if( $b ){
2334 $c = $this->NewObj5('Less_Tree_Condition',array($op[0], $a, $b, $index, $negate));
2335 } else {
2336 $this->Error('Unexpected expression');
2338 } else {
2339 $k = $this->NewObj1('Less_Tree_Keyword','true');
2340 $c = $this->NewObj5('Less_Tree_Condition',array('=', $a, $k, $index, $negate));
2342 $this->expectChar(')');
2343 return $this->MatchReg('/\\Gand/') ? $this->NewObj3('Less_Tree_Condition',array('and', $c, $this->parseCondition())) : $c;
2348 * An operand is anything that can be part of an operation,
2349 * such as a Color, or a Variable
2352 private function parseOperand (){
2354 $negate = false;
2355 $offset = $this->pos+1;
2356 if( $offset >= $this->input_len ){
2357 return;
2359 $char = $this->input[$offset];
2360 if( $char === '@' || $char === '(' ){
2361 $negate = $this->MatchChar('-');
2364 $o = $this->MatchFuncs(array('parseSub','parseEntitiesDimension','parseEntitiesColor','parseEntitiesVariable','parseEntitiesCall'));
2366 if( $negate ){
2367 $o->parensInOp = true;
2368 $o = $this->NewObj1('Less_Tree_Negative',$o);
2371 return $o;
2376 * Expressions either represent mathematical operations,
2377 * or white-space delimited Entities.
2379 * 1px solid black
2380 * @var * 2
2382 * @return Less_Tree_Expression|null
2384 private function parseExpression (){
2385 $entities = array();
2388 $e = $this->MatchFuncs(array('parseAddition','parseEntity'));
2389 if( $e ){
2390 $entities[] = $e;
2391 // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here
2392 if( !$this->PeekReg('/\\G\/[\/*]/') ){
2393 $delim = $this->MatchChar('/');
2394 if( $delim ){
2395 $entities[] = $this->NewObj1('Less_Tree_Anonymous',$delim);
2399 }while($e);
2401 if( $entities ){
2402 return $this->NewObj1('Less_Tree_Expression',$entities);
2408 * Parse a property
2409 * eg: 'min-width', 'orientation', etc
2411 * @return string
2413 private function parseProperty (){
2414 $name = $this->MatchReg('/\\G(\*?-?[_a-zA-Z0-9-]+)\s*:/');
2415 if( $name ){
2416 return $name[1];
2422 * Parse a rule property
2423 * eg: 'color', 'width', 'height', etc
2425 * @return string
2427 private function parseRuleProperty(){
2428 $offset = $this->pos;
2429 $name = array();
2430 $index = array();
2431 $length = 0;
2434 $this->rulePropertyMatch('/\\G(\*?)/', $offset, $length, $index, $name );
2435 while( $this->rulePropertyMatch('/\\G((?:[\w-]+)|(?:@\{[\w-]+\}))/', $offset, $length, $index, $name )); // !
2437 if( (count($name) > 1) && $this->rulePropertyMatch('/\\G\s*((?:\+_|\+)?)\s*:/', $offset, $length, $index, $name) ){
2438 // at last, we have the complete match now. move forward,
2439 // convert name particles to tree objects and return:
2440 $this->skipWhitespace($length);
2442 if( $name[0] === '' ){
2443 array_shift($name);
2444 array_shift($index);
2446 foreach($name as $k => $s ){
2447 if( !$s || $s[0] !== '@' ){
2448 $name[$k] = $this->NewObj1('Less_Tree_Keyword',$s);
2449 }else{
2450 $name[$k] = $this->NewObj3('Less_Tree_Variable',array('@' . substr($s,2,-1), $index[$k], $this->env->currentFileInfo));
2453 return $name;
2459 private function rulePropertyMatch( $re, &$offset, &$length, &$index, &$name ){
2460 preg_match($re, $this->input, $a, 0, $offset);
2461 if( $a ){
2462 $index[] = $this->pos + $length;
2463 $length += strlen($a[0]);
2464 $offset += strlen($a[0]);
2465 $name[] = $a[1];
2466 return true;
2470 public static function serializeVars( $vars ){
2471 $s = '';
2473 foreach($vars as $name => $value){
2474 $s .= (($name[0] === '@') ? '' : '@') . $name .': '. $value . ((substr($value,-1) === ';') ? '' : ';');
2477 return $s;
2482 * Some versions of php have trouble with method_exists($a,$b) if $a is not an object
2484 * @param string $b
2486 public static function is_method($a,$b){
2487 return is_object($a) && method_exists($a,$b);
2492 * Round numbers similarly to javascript
2493 * eg: 1.499999 to 1 instead of 2
2496 public static function round($i, $precision = 0){
2498 $precision = pow(10,$precision);
2499 $i = $i*$precision;
2501 $ceil = ceil($i);
2502 $floor = floor($i);
2503 if( ($ceil - $i) <= ($i - $floor) ){
2504 return $ceil/$precision;
2505 }else{
2506 return $floor/$precision;
2512 * Create Less_Tree_* objects and optionally generate a cache string
2514 * @return mixed
2516 public function NewObj0($class){
2517 $obj = new $class();
2518 if( $this->CacheEnabled() ){
2519 $obj->cache_string = ' new '.$class.'()';
2521 return $obj;
2524 public function NewObj1($class, $arg){
2525 $obj = new $class( $arg );
2526 if( $this->CacheEnabled() ){
2527 $obj->cache_string = ' new '.$class.'('.Less_Parser::ArgString($arg).')';
2529 return $obj;
2532 public function NewObj2($class, $args){
2533 $obj = new $class( $args[0], $args[1] );
2534 if( $this->CacheEnabled() ){
2535 $this->ObjCache( $obj, $class, $args);
2537 return $obj;
2540 public function NewObj3($class, $args){
2541 $obj = new $class( $args[0], $args[1], $args[2] );
2542 if( $this->CacheEnabled() ){
2543 $this->ObjCache( $obj, $class, $args);
2545 return $obj;
2548 public function NewObj4($class, $args){
2549 $obj = new $class( $args[0], $args[1], $args[2], $args[3] );
2550 if( $this->CacheEnabled() ){
2551 $this->ObjCache( $obj, $class, $args);
2553 return $obj;
2556 public function NewObj5($class, $args){
2557 $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4] );
2558 if( $this->CacheEnabled() ){
2559 $this->ObjCache( $obj, $class, $args);
2561 return $obj;
2564 public function NewObj6($class, $args){
2565 $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5] );
2566 if( $this->CacheEnabled() ){
2567 $this->ObjCache( $obj, $class, $args);
2569 return $obj;
2572 public function NewObj7($class, $args){
2573 $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6] );
2574 if( $this->CacheEnabled() ){
2575 $this->ObjCache( $obj, $class, $args);
2577 return $obj;
2580 //caching
2581 public function ObjCache($obj, $class, $args=array()){
2582 $obj->cache_string = ' new '.$class.'('. self::ArgCache($args).')';
2585 public function ArgCache($args){
2586 return implode(',',array_map( array('Less_Parser','ArgString'),$args));
2591 * Convert an argument to a string for use in the parser cache
2593 * @return string
2595 public static function ArgString($arg){
2597 $type = gettype($arg);
2599 if( $type === 'object'){
2600 $string = $arg->cache_string;
2601 unset($arg->cache_string);
2602 return $string;
2604 }elseif( $type === 'array' ){
2605 $string = ' Array(';
2606 foreach($arg as $k => $a){
2607 $string .= var_export($k,true).' => '.self::ArgString($a).',';
2609 return $string . ')';
2612 return var_export($arg,true);
2615 public function Error($msg){
2616 throw new Less_Exception_Parser($msg, null, $this->furthest, $this->env->currentFileInfo);
2619 public static function WinPath($path){
2620 return str_replace('\\', '/', $path);
2623 public function CacheEnabled(){
2624 return (Less_Parser::$options['cache_method'] && (Less_Cache::$cache_dir || (Less_Parser::$options['cache_method'] == 'callback')));