Merge branch 'MDL-44773-27' of git://github.com/FMCorz/moodle into MOODLE_27_STABLE
[moodle.git] / lib / lessphp / Parser.php
blob0bfdfad6269efc4cf89793f6b28ac310237cd029
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';
31 'sourceMap' => false, // whether to output a source map
32 'sourceMapBasepath' => null,
33 'sourceMapWriteTo' => null,
34 'sourceMapURL' => null,
36 'plugins' => array(),
40 public static $options = array();
43 private $input; // Less input string
44 private $input_len; // input string length
45 private $pos; // current index in `input`
46 private $saveStack = array(); // holds state for backtracking
47 private $furthest;
49 /**
50 * @var Less_Environment
52 private $env;
54 private $rules = array();
56 private static $imports = array();
58 public static $has_extends = false;
60 public static $next_id = 0;
62 /**
63 * Filename to contents of all parsed the files
65 * @var array
67 public static $contentsMap = array();
70 /**
71 * @param Less_Environment|array|null $env
73 public function __construct( $env = null ){
75 // Top parser on an import tree must be sure there is one "env"
76 // which will then be passed around by reference.
77 if( $env instanceof Less_Environment ){
78 $this->env = $env;
79 }else{
80 $this->SetOptions(Less_Parser::$default_options);
81 $this->Reset( $env );
87 /**
88 * Reset the parser state completely
91 public function Reset( $options = null ){
92 $this->rules = array();
93 self::$imports = array();
94 self::$has_extends = false;
95 self::$imports = array();
96 self::$contentsMap = array();
98 $this->env = new Less_Environment($options);
99 $this->env->Init();
101 //set new options
102 if( is_array($options) ){
103 $this->SetOptions(Less_Parser::$default_options);
104 $this->SetOptions($options);
109 * Set one or more compiler options
110 * options: import_dirs, cache_dir, cache_method
113 public function SetOptions( $options ){
114 foreach($options as $option => $value){
115 $this->SetOption($option,$value);
120 * Set one compiler option
123 public function SetOption($option,$value){
125 switch($option){
127 case 'import_dirs':
128 $this->SetImportDirs($value);
129 return;
131 case 'cache_dir':
132 if( is_string($value) ){
133 Less_Cache::SetCacheDir($value);
134 Less_Cache::CheckCacheDir();
136 return;
139 Less_Parser::$options[$option] = $value;
146 * Get the current css buffer
148 * @return string
150 public function getCss(){
152 $precision = ini_get('precision');
153 @ini_set('precision',16);
154 $locale = setlocale(LC_NUMERIC, 0);
155 setlocale(LC_NUMERIC, "C");
158 $root = new Less_Tree_Ruleset(array(), $this->rules );
159 $root->root = true;
160 $root->firstRoot = true;
163 $this->PreVisitors($root);
165 self::$has_extends = false;
166 $evaldRoot = $root->compile($this->env);
170 $this->PostVisitors($evaldRoot);
172 if( Less_Parser::$options['sourceMap'] ){
173 $generator = new Less_SourceMap_Generator($evaldRoot, Less_Parser::$contentsMap, Less_Parser::$options );
174 // will also save file
175 // FIXME: should happen somewhere else?
176 $css = $generator->generateCSS();
177 }else{
178 $css = $evaldRoot->toCSS();
181 if( Less_Parser::$options['compress'] ){
182 $css = preg_replace('/(^(\s)+)|((\s)+$)/', '', $css);
185 //reset php settings
186 @ini_set('precision',$precision);
187 setlocale(LC_NUMERIC, $locale);
189 return $css;
193 * Run pre-compile visitors
196 private function PreVisitors($root){
198 if( Less_Parser::$options['plugins'] ){
199 foreach(Less_Parser::$options['plugins'] as $plugin){
200 if( !empty($plugin->isPreEvalVisitor) ){
201 $plugin->run($root);
209 * Run post-compile visitors
212 private function PostVisitors($evaldRoot){
214 $visitors = array();
215 $visitors[] = new Less_Visitor_joinSelector();
216 if( self::$has_extends ){
217 $visitors[] = new Less_Visitor_processExtends();
219 $visitors[] = new Less_Visitor_toCSS();
222 if( Less_Parser::$options['plugins'] ){
223 foreach(Less_Parser::$options['plugins'] as $plugin){
224 if( property_exists($plugin,'isPreEvalVisitor') && $plugin->isPreEvalVisitor ){
225 continue;
228 if( property_exists($plugin,'isPreVisitor') && $plugin->isPreVisitor ){
229 array_unshift( $visitors, $plugin);
230 }else{
231 $visitors[] = $plugin;
237 for($i = 0; $i < count($visitors); $i++ ){
238 $visitors[$i]->run($evaldRoot);
245 * Parse a Less string into css
247 * @param string $str The string to convert
248 * @param string $uri_root The url of the file
249 * @return Less_Tree_Ruleset|Less_Parser
251 public function parse( $str, $file_uri = null ){
253 if( !$file_uri ){
254 $uri_root = '';
255 $filename = 'anonymous-file-'.Less_Parser::$next_id++.'.less';
256 }else{
257 $file_uri = self::WinPath($file_uri);
258 $filename = basename($file_uri);
259 $uri_root = dirname($file_uri);
262 $previousFileInfo = $this->env->currentFileInfo;
263 $uri_root = self::WinPath($uri_root);
264 $this->SetFileInfo($filename, $uri_root);
266 $this->input = $str;
267 $this->_parse();
269 if( $previousFileInfo ){
270 $this->env->currentFileInfo = $previousFileInfo;
273 return $this;
278 * Parse a Less string from a given file
280 * @throws Less_Exception_Parser
281 * @param string $filename The file to parse
282 * @param string $uri_root The url of the file
283 * @param bool $returnRoot Indicates whether the return value should be a css string a root node
284 * @return Less_Tree_Ruleset|Less_Parser
286 public function parseFile( $filename, $uri_root = '', $returnRoot = false){
288 if( !file_exists($filename) ){
289 $this->Error(sprintf('File `%s` not found.', $filename));
293 // fix uri_root?
294 // Instead of The mixture of file path for the first argument and directory path for the second argument has bee
295 if( !$returnRoot && !empty($uri_root) && basename($uri_root) == basename($filename) ){
296 $uri_root = dirname($uri_root);
300 $previousFileInfo = $this->env->currentFileInfo;
301 $filename = self::WinPath($filename);
302 $uri_root = self::WinPath($uri_root);
303 $this->SetFileInfo($filename, $uri_root);
305 self::AddParsedFile($filename);
307 if( $returnRoot ){
308 $rules = $this->GetRules( $filename );
309 $return = new Less_Tree_Ruleset(array(), $rules );
310 }else{
311 $this->_parse( $filename );
312 $return = $this;
315 if( $previousFileInfo ){
316 $this->env->currentFileInfo = $previousFileInfo;
319 return $return;
324 * Allows a user to set variables values
325 * @param array $vars
326 * @return Less_Parser
328 public function ModifyVars( $vars ){
330 $this->input = $this->serializeVars( $vars );
331 $this->_parse();
333 return $this;
338 * @param string $filename
340 public function SetFileInfo( $filename, $uri_root = ''){
342 $filename = Less_Environment::normalizePath($filename);
343 $dirname = preg_replace('/[^\/\\\\]*$/','',$filename);
345 if( !empty($uri_root) ){
346 $uri_root = rtrim($uri_root,'/').'/';
349 $currentFileInfo = array();
351 //entry info
352 if( isset($this->env->currentFileInfo) ){
353 $currentFileInfo['entryPath'] = $this->env->currentFileInfo['entryPath'];
354 $currentFileInfo['entryUri'] = $this->env->currentFileInfo['entryUri'];
355 $currentFileInfo['rootpath'] = $this->env->currentFileInfo['rootpath'];
357 }else{
358 $currentFileInfo['entryPath'] = $dirname;
359 $currentFileInfo['entryUri'] = $uri_root;
360 $currentFileInfo['rootpath'] = $dirname;
363 $currentFileInfo['currentDirectory'] = $dirname;
364 $currentFileInfo['currentUri'] = $uri_root.basename($filename);
365 $currentFileInfo['filename'] = $filename;
366 $currentFileInfo['uri_root'] = $uri_root;
369 //inherit reference
370 if( isset($this->env->currentFileInfo['reference']) && $this->env->currentFileInfo['reference'] ){
371 $currentFileInfo['reference'] = true;
374 $this->env->currentFileInfo = $currentFileInfo;
379 * @deprecated 1.5.1.2
382 public function SetCacheDir( $dir ){
384 if( !file_exists($dir) ){
385 if( mkdir($dir) ){
386 return true;
388 throw new Less_Exception_Parser('Less.php cache directory couldn\'t be created: '.$dir);
390 }elseif( !is_dir($dir) ){
391 throw new Less_Exception_Parser('Less.php cache directory doesn\'t exist: '.$dir);
393 }elseif( !is_writable($dir) ){
394 throw new Less_Exception_Parser('Less.php cache directory isn\'t writable: '.$dir);
396 }else{
397 $dir = self::WinPath($dir);
398 Less_Cache::$cache_dir = rtrim($dir,'/').'/';
399 return true;
405 * Set a list of directories or callbacks the parser should use for determining import paths
407 * @param array $dirs
409 public function SetImportDirs( $dirs ){
410 Less_Parser::$options['import_dirs'] = array();
412 foreach($dirs as $path => $uri_root){
414 $path = self::WinPath($path);
415 if( !empty($path) ){
416 $path = rtrim($path,'/').'/';
419 if ( !is_callable($uri_root) ){
420 $uri_root = self::WinPath($uri_root);
421 if( !empty($uri_root) ){
422 $uri_root = rtrim($uri_root,'/').'/';
426 Less_Parser::$options['import_dirs'][$path] = $uri_root;
431 * @param string $file_path
433 private function _parse( $file_path = null ){
434 $this->rules = array_merge($this->rules, $this->GetRules( $file_path ));
439 * Return the results of parsePrimary for $file_path
440 * Use cache and save cached results if possible
442 * @param string|null $file_path
444 private function GetRules( $file_path ){
446 $this->SetInput($file_path);
448 $cache_file = $this->CacheFile( $file_path );
449 if( $cache_file && file_exists($cache_file) ){
450 switch(Less_Parser::$options['cache_method']){
452 // Using serialize
453 // Faster but uses more memory
454 case 'serialize':
455 $cache = unserialize(file_get_contents($cache_file));
456 if( $cache ){
457 touch($cache_file);
458 $this->UnsetInput();
459 return $cache;
461 break;
464 // Using generated php code
465 case 'var_export':
466 case 'php':
467 $this->UnsetInput();
468 return include($cache_file);
472 $rules = $this->parsePrimary();
474 if( $this->pos < $this->input_len ){
475 throw new Less_Exception_Chunk($this->input, null, $this->furthest, $this->env->currentFileInfo);
478 $this->UnsetInput();
481 //save the cache
482 if( $cache_file ){
484 //msg('write cache file');
485 switch(Less_Parser::$options['cache_method']){
486 case 'serialize':
487 file_put_contents( $cache_file, serialize($rules) );
488 break;
489 case 'php':
490 file_put_contents( $cache_file, '<?php return '.self::ArgString($rules).'; ?>' );
491 break;
492 case 'var_export':
493 //Requires __set_state()
494 file_put_contents( $cache_file, '<?php return '.var_export($rules,true).'; ?>' );
495 break;
498 Less_Cache::CleanCache();
501 return $rules;
506 * Set up the input buffer
509 public function SetInput( $file_path ){
511 if( $file_path ){
512 $this->input = file_get_contents( $file_path );
515 $this->pos = $this->furthest = 0;
517 // Remove potential UTF Byte Order Mark
518 $this->input = preg_replace('/\\G\xEF\xBB\xBF/', '', $this->input);
519 $this->input_len = strlen($this->input);
522 if( Less_Parser::$options['sourceMap'] && $this->env->currentFileInfo ){
523 $uri = $this->env->currentFileInfo['currentUri'];
524 Less_Parser::$contentsMap[$uri] = $this->input;
531 * Free up some memory
534 public function UnsetInput(){
535 unset($this->input, $this->pos, $this->input_len, $this->furthest);
536 $this->saveStack = array();
540 public function CacheFile( $file_path ){
542 if( $file_path && Less_Parser::$options['cache_method'] && Less_Cache::$cache_dir ){
544 $env = get_object_vars($this->env);
545 unset($env['frames']);
547 $parts = array();
548 $parts[] = $file_path;
549 $parts[] = filesize( $file_path );
550 $parts[] = filemtime( $file_path );
551 $parts[] = $env;
552 $parts[] = Less_Version::cache_version;
553 $parts[] = Less_Parser::$options['cache_method'];
554 return Less_Cache::$cache_dir.'lessphp_'.base_convert( sha1(json_encode($parts) ), 16, 36).'.lesscache';
559 static function AddParsedFile($file){
560 self::$imports[] = $file;
563 static function AllParsedFiles(){
564 return self::$imports;
568 * @param string $file
570 static function FileParsed($file){
571 return in_array($file,self::$imports);
575 function save() {
576 $this->saveStack[] = $this->pos;
579 private function restore() {
580 $this->pos = array_pop($this->saveStack);
583 private function forget(){
584 array_pop($this->saveStack);
588 private function isWhitespace($offset = 0) {
589 return preg_match('/\s/',$this->input[ $this->pos + $offset]);
593 * Parse from a token, regexp or string, and move forward if match
595 * @param array $toks
596 * @return array
598 private function match($toks){
600 // The match is confirmed, add the match length to `this::pos`,
601 // and consume any extra white-space characters (' ' || '\n')
602 // which come after that. The reason for this is that LeSS's
603 // grammar is mostly white-space insensitive.
606 foreach($toks as $tok){
608 $char = $tok[0];
610 if( $char === '/' ){
611 $match = $this->MatchReg($tok);
613 if( $match ){
614 return count($match) === 1 ? $match[0] : $match;
617 }elseif( $char === '#' ){
618 $match = $this->MatchChar($tok[1]);
620 }else{
621 // Non-terminal, match using a function call
622 $match = $this->$tok();
626 if( $match ){
627 return $match;
633 * @param string[] $toks
635 * @return string
637 private function MatchFuncs($toks){
639 foreach($toks as $tok){
640 $match = $this->$tok();
641 if( $match ){
642 return $match;
648 // Match a single character in the input,
649 private function MatchChar($tok){
650 if( ($this->pos < $this->input_len) && ($this->input[$this->pos] === $tok) ){
651 $this->skipWhitespace(1);
652 return $tok;
656 // Match a regexp from the current start point
657 private function MatchReg($tok){
659 if( preg_match($tok, $this->input, $match, 0, $this->pos) ){
660 $this->skipWhitespace(strlen($match[0]));
661 return $match;
667 * Same as match(), but don't change the state of the parser,
668 * just return the match.
670 * @param string $tok
671 * @return integer
673 public function PeekReg($tok){
674 return preg_match($tok, $this->input, $match, 0, $this->pos);
678 * @param string $tok
680 public function PeekChar($tok){
681 //return ($this->input[$this->pos] === $tok );
682 return ($this->pos < $this->input_len) && ($this->input[$this->pos] === $tok );
687 * @param integer $length
689 public function skipWhitespace($length){
691 $this->pos += $length;
693 for(; $this->pos < $this->input_len; $this->pos++ ){
694 $c = $this->input[$this->pos];
696 if( ($c !== "\n") && ($c !== "\r") && ($c !== "\t") && ($c !== ' ') ){
697 break;
704 * @param string $tok
705 * @param string|null $msg
707 public function expect($tok, $msg = NULL) {
708 $result = $this->match( array($tok) );
709 if (!$result) {
710 $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg );
711 } else {
712 return $result;
717 * @param string $tok
719 public function expectChar($tok, $msg = null ){
720 $result = $this->MatchChar($tok);
721 if( !$result ){
722 $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg );
723 }else{
724 return $result;
729 // Here in, the parsing rules/functions
731 // The basic structure of the syntax tree generated is as follows:
733 // Ruleset -> Rule -> Value -> Expression -> Entity
735 // Here's some LESS code:
737 // .class {
738 // color: #fff;
739 // border: 1px solid #000;
740 // width: @w + 4px;
741 // > .child {...}
742 // }
744 // And here's what the parse tree might look like:
746 // Ruleset (Selector '.class', [
747 // Rule ("color", Value ([Expression [Color #fff]]))
748 // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]]))
749 // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]]))
750 // Ruleset (Selector [Element '>', '.child'], [...])
751 // ])
753 // In general, most rules will try to parse a token with the `$()` function, and if the return
754 // value is truly, will return a new node, of the relevant type. Sometimes, we need to check
755 // first, before parsing, that's when we use `peek()`.
759 // The `primary` rule is the *entry* and *exit* point of the parser.
760 // The rules here can appear at any level of the parse tree.
762 // The recursive nature of the grammar is an interplay between the `block`
763 // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,
764 // as represented by this simplified grammar:
766 // primary → (ruleset | rule)+
767 // ruleset → selector+ block
768 // block → '{' primary '}'
770 // Only at one point is the primary rule not called from the
771 // block rule: at the root level.
773 private function parsePrimary(){
774 $root = array();
776 while( true ){
778 if( $this->pos >= $this->input_len ){
779 break;
782 $node = $this->parseExtend(true);
783 if( $node ){
784 $root = array_merge($root,$node);
785 continue;
788 //$node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseDirective'));
789 $node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseNameValue', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseRulesetCall', 'parseDirective'));
791 if( $node ){
792 $root[] = $node;
793 }elseif( !$this->MatchReg('/\\G[\s\n;]+/') ){
794 break;
797 if( $this->PeekChar('}') ){
798 break;
802 return $root;
807 // We create a Comment node for CSS comments `/* */`,
808 // but keep the LeSS comments `//` silent, by just skipping
809 // over them.
810 private function parseComment(){
812 if( $this->input[$this->pos] !== '/' ){
813 return;
816 if( $this->input[$this->pos+1] === '/' ){
817 $match = $this->MatchReg('/\\G\/\/.*/');
818 return $this->NewObj4('Less_Tree_Comment',array($match[0], true, $this->pos, $this->env->currentFileInfo));
821 //$comment = $this->MatchReg('/\\G\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/');
822 $comment = $this->MatchReg('/\\G\/\*(?s).*?\*+\/\n?/');//not the same as less.js to prevent fatal errors
823 if( $comment ){
824 return $this->NewObj4('Less_Tree_Comment',array($comment[0], false, $this->pos, $this->env->currentFileInfo));
828 private function parseComments(){
829 $comments = array();
831 while( $this->pos < $this->input_len ){
832 $comment = $this->parseComment();
833 if( !$comment ){
834 break;
837 $comments[] = $comment;
840 return $comments;
846 // A string, which supports escaping " and '
848 // "milky way" 'he\'s the one!'
850 private function parseEntitiesQuoted() {
851 $j = $this->pos;
852 $e = false;
853 $index = $this->pos;
855 if( $this->input[$this->pos] === '~' ){
856 $j++;
857 $e = true; // Escaped strings
860 if( $this->input[$j] != '"' && $this->input[$j] !== "'" ){
861 return;
864 if ($e) {
865 $this->MatchChar('~');
867 $str = $this->MatchReg('/\\G"((?:[^"\\\\\r\n]|\\\\.)*)"|\'((?:[^\'\\\\\r\n]|\\\\.)*)\'/');
868 if( $str ){
869 $result = $str[0][0] == '"' ? $str[1] : $str[2];
870 return $this->NewObj5('Less_Tree_Quoted',array($str[0], $result, $e, $index, $this->env->currentFileInfo) );
872 return;
877 // A catch-all word, such as:
879 // black border-collapse
881 private function parseEntitiesKeyword(){
883 //$k = $this->MatchReg('/\\G[_A-Za-z-][_A-Za-z0-9-]*/');
884 $k = $this->MatchReg('/\\G%|\\G[_A-Za-z-][_A-Za-z0-9-]*/');
885 if( $k ){
886 $k = $k[0];
887 $color = $this->fromKeyword($k);
888 if( $color ){
889 return $color;
891 return $this->NewObj1('Less_Tree_Keyword',$k);
895 // duplicate of Less_Tree_Color::FromKeyword
896 private function FromKeyword( $keyword ){
897 $keyword = strtolower($keyword);
899 if( Less_Colors::hasOwnProperty($keyword) ){
900 // detect named color
901 return $this->NewObj1('Less_Tree_Color',substr(Less_Colors::color($keyword), 1));
904 if( $keyword === 'transparent' ){
905 return $this->NewObj3('Less_Tree_Color', array( array(0, 0, 0), 0, true));
910 // A function call
912 // rgb(255, 0, 255)
914 // We also try to catch IE's `alpha()`, but let the `alpha` parser
915 // deal with the details.
917 // The arguments are parsed with the `entities.arguments` parser.
919 private function parseEntitiesCall(){
920 $index = $this->pos;
922 if( !preg_match('/\\G([\w-]+|%|progid:[\w\.]+)\(/', $this->input, $name,0,$this->pos) ){
923 return;
925 $name = $name[1];
926 $nameLC = strtolower($name);
928 if ($nameLC === 'url') {
929 return null;
932 $this->pos += strlen($name);
934 if( $nameLC === 'alpha' ){
935 $alpha_ret = $this->parseAlpha();
936 if( $alpha_ret ){
937 return $alpha_ret;
941 $this->MatchChar('('); // Parse the '(' and consume whitespace.
943 $args = $this->parseEntitiesArguments();
945 if( !$this->MatchChar(')') ){
946 return;
949 if ($name) {
950 return $this->NewObj4('Less_Tree_Call',array($name, $args, $index, $this->env->currentFileInfo) );
955 * Parse a list of arguments
957 * @return array
959 private function parseEntitiesArguments(){
961 $args = array();
962 while( true ){
963 $arg = $this->MatchFuncs( array('parseEntitiesAssignment','parseExpression') );
964 if( !$arg ){
965 break;
968 $args[] = $arg;
969 if( !$this->MatchChar(',') ){
970 break;
973 return $args;
976 private function parseEntitiesLiteral(){
977 return $this->MatchFuncs( array('parseEntitiesDimension','parseEntitiesColor','parseEntitiesQuoted','parseUnicodeDescriptor') );
980 // Assignments are argument entities for calls.
981 // They are present in ie filter properties as shown below.
983 // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* )
985 private function parseEntitiesAssignment() {
987 $key = $this->MatchReg('/\\G\w+(?=\s?=)/');
988 if( !$key ){
989 return;
992 if( !$this->MatchChar('=') ){
993 return;
996 $value = $this->parseEntity();
997 if( $value ){
998 return $this->NewObj2('Less_Tree_Assignment',array($key[0], $value));
1003 // Parse url() tokens
1005 // We use a specific rule for urls, because they don't really behave like
1006 // standard function calls. The difference is that the argument doesn't have
1007 // to be enclosed within a string, so it can't be parsed as an Expression.
1009 private function parseEntitiesUrl(){
1012 if( $this->input[$this->pos] !== 'u' || !$this->matchReg('/\\Gurl\(/') ){
1013 return;
1016 $value = $this->match( array('parseEntitiesQuoted','parseEntitiesVariable','/\\Gdata\:.*?[^\)]+/','/\\G(?:(?:\\\\[\(\)\'"])|[^\(\)\'"])+/') );
1017 if( !$value ){
1018 $value = '';
1022 $this->expectChar(')');
1025 if( isset($value->value) || $value instanceof Less_Tree_Variable ){
1026 return $this->NewObj2('Less_Tree_Url',array($value, $this->env->currentFileInfo));
1029 return $this->NewObj2('Less_Tree_Url', array( $this->NewObj1('Less_Tree_Anonymous',$value), $this->env->currentFileInfo) );
1034 // A Variable entity, such as `@fink`, in
1036 // width: @fink + 2px
1038 // We use a different parser for variable definitions,
1039 // see `parsers.variable`.
1041 private function parseEntitiesVariable(){
1042 $index = $this->pos;
1043 if ($this->PeekChar('@') && ($name = $this->MatchReg('/\\G@@?[\w-]+/'))) {
1044 return $this->NewObj3('Less_Tree_Variable', array( $name[0], $index, $this->env->currentFileInfo));
1049 // A variable entity useing the protective {} e.g. @{var}
1050 private function parseEntitiesVariableCurly() {
1051 $index = $this->pos;
1053 if( $this->input_len > ($this->pos+1) && $this->input[$this->pos] === '@' && ($curly = $this->MatchReg('/\\G@\{([\w-]+)\}/')) ){
1054 return $this->NewObj3('Less_Tree_Variable',array('@'.$curly[1], $index, $this->env->currentFileInfo));
1059 // A Hexadecimal color
1061 // #4F3C2F
1063 // `rgb` and `hsl` colors are parsed through the `entities.call` parser.
1065 private function parseEntitiesColor(){
1066 if ($this->PeekChar('#') && ($rgb = $this->MatchReg('/\\G#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/'))) {
1067 return $this->NewObj1('Less_Tree_Color',$rgb[1]);
1072 // A Dimension, that is, a number and a unit
1074 // 0.5em 95%
1076 private function parseEntitiesDimension(){
1078 $c = @ord($this->input[$this->pos]);
1080 //Is the first char of the dimension 0-9, '.', '+' or '-'
1081 if (($c > 57 || $c < 43) || $c === 47 || $c == 44){
1082 return;
1085 $value = $this->MatchReg('/\\G([+-]?\d*\.?\d+)(%|[a-z]+)?/');
1086 if( $value ){
1088 if( isset($value[2]) ){
1089 return $this->NewObj2('Less_Tree_Dimension', array($value[1],$value[2]));
1091 return $this->NewObj1('Less_Tree_Dimension',$value[1]);
1097 // A unicode descriptor, as is used in unicode-range
1099 // U+0?? or U+00A1-00A9
1101 function parseUnicodeDescriptor() {
1102 $ud = $this->MatchReg('/\\G(U\+[0-9a-fA-F?]+)(\-[0-9a-fA-F?]+)?/');
1103 if( $ud ){
1104 return $this->NewObj1('Less_Tree_UnicodeDescriptor', $ud[0]);
1110 // JavaScript code to be evaluated
1112 // `window.location.href`
1114 private function parseEntitiesJavascript(){
1115 $e = false;
1116 $j = $this->pos;
1117 if( $this->input[$j] === '~' ){
1118 $j++;
1119 $e = true;
1121 if( $this->input[$j] !== '`' ){
1122 return;
1124 if( $e ){
1125 $this->MatchChar('~');
1127 $str = $this->MatchReg('/\\G`([^`]*)`/');
1128 if( $str ){
1129 return $this->NewObj3('Less_Tree_Javascript', array($str[1], $this->pos, $e));
1135 // The variable part of a variable definition. Used in the `rule` parser
1137 // @fink:
1139 private function parseVariable(){
1140 if ($this->PeekChar('@') && ($name = $this->MatchReg('/\\G(@[\w-]+)\s*:/'))) {
1141 return $name[1];
1147 // The variable part of a variable definition. Used in the `rule` parser
1149 // @fink();
1151 private function parseRulesetCall(){
1153 if( $this->input[$this->pos] === '@' && ($name = $this->MatchReg('/\\G(@[\w-]+)\s*\(\s*\)\s*;/')) ){
1154 return $this->NewObj1('Less_Tree_RulesetCall', $name[1] );
1160 // extend syntax - used to extend selectors
1162 function parseExtend($isRule = false){
1164 $index = $this->pos;
1165 $extendList = array();
1168 if( !$this->MatchReg( $isRule ? '/\\G&:extend\(/' : '/\\G:extend\(/' ) ){ return; }
1171 $option = null;
1172 $elements = array();
1173 while( true ){
1174 $option = $this->MatchReg('/\\G(all)(?=\s*(\)|,))/');
1175 if( $option ){ break; }
1176 $e = $this->parseElement();
1177 if( !$e ){ break; }
1178 $elements[] = $e;
1181 if( $option ){
1182 $option = $option[1];
1185 $extendList[] = $this->NewObj3('Less_Tree_Extend', array( $this->NewObj1('Less_Tree_Selector',$elements), $option, $index ));
1187 }while( $this->MatchChar(",") );
1189 $this->expect('/\\G\)/');
1191 if( $isRule ){
1192 $this->expect('/\\G;/');
1195 return $extendList;
1200 // A Mixin call, with an optional argument list
1202 // #mixins > .square(#fff);
1203 // .rounded(4px, black);
1204 // .button;
1206 // The `while` loop is there because mixins can be
1207 // namespaced, but we only support the child and descendant
1208 // selector for now.
1210 private function parseMixinCall(){
1212 $char = $this->input[$this->pos];
1213 if( $char !== '.' && $char !== '#' ){
1214 return;
1217 $index = $this->pos;
1218 $this->save(); // stop us absorbing part of an invalid selector
1220 $elements = $this->parseMixinCallElements();
1222 if( $elements ){
1224 if( $this->MatchChar('(') ){
1225 $returned = $this->parseMixinArgs(true);
1226 $args = $returned['args'];
1227 $this->expectChar(')');
1228 }else{
1229 $args = array();
1232 $important = $this->parseImportant();
1234 if( $this->parseEnd() ){
1235 $this->forget();
1236 return $this->NewObj5('Less_Tree_Mixin_Call', array( $elements, $args, $index, $this->env->currentFileInfo, $important));
1240 $this->restore();
1244 private function parseMixinCallElements(){
1245 $elements = array();
1246 $c = null;
1248 while( true ){
1249 $elemIndex = $this->pos;
1250 $e = $this->MatchReg('/\\G[#.](?:[\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/');
1251 if( !$e ){
1252 break;
1254 $elements[] = $this->NewObj4('Less_Tree_Element', array($c, $e[0], $elemIndex, $this->env->currentFileInfo));
1255 $c = $this->MatchChar('>');
1258 return $elements;
1264 * @param boolean $isCall
1266 private function parseMixinArgs( $isCall ){
1267 $expressions = array();
1268 $argsSemiColon = array();
1269 $isSemiColonSeperated = null;
1270 $argsComma = array();
1271 $expressionContainsNamed = null;
1272 $name = null;
1273 $returner = array('args'=>array(), 'variadic'=> false);
1275 $this->save();
1277 while( true ){
1278 if( $isCall ){
1279 $arg = $this->MatchFuncs( array( 'parseDetachedRuleset','parseExpression' ) );
1280 } else {
1281 $this->parseComments();
1282 if( $this->input[ $this->pos ] === '.' && $this->MatchReg('/\\G\.{3}/') ){
1283 $returner['variadic'] = true;
1284 if( $this->MatchChar(";") && !$isSemiColonSeperated ){
1285 $isSemiColonSeperated = true;
1288 if( $isSemiColonSeperated ){
1289 $argsSemiColon[] = array('variadic'=>true);
1290 }else{
1291 $argsComma[] = array('variadic'=>true);
1293 break;
1295 $arg = $this->MatchFuncs( array('parseEntitiesVariable','parseEntitiesLiteral','parseEntitiesKeyword') );
1298 if( !$arg ){
1299 break;
1303 $nameLoop = null;
1304 if( $arg instanceof Less_Tree_Expression ){
1305 $arg->throwAwayComments();
1307 $value = $arg;
1308 $val = null;
1310 if( $isCall ){
1311 // Variable
1312 if( property_exists($arg,'value') && count($arg->value) == 1 ){
1313 $val = $arg->value[0];
1315 } else {
1316 $val = $arg;
1320 if( $val instanceof Less_Tree_Variable ){
1322 if( $this->MatchChar(':') ){
1323 if( $expressions ){
1324 if( $isSemiColonSeperated ){
1325 $this->Error('Cannot mix ; and , as delimiter types');
1327 $expressionContainsNamed = true;
1330 // we do not support setting a ruleset as a default variable - it doesn't make sense
1331 // However if we do want to add it, there is nothing blocking it, just don't error
1332 // and remove isCall dependency below
1333 $value = null;
1334 if( $isCall ){
1335 $value = $this->parseDetachedRuleset();
1337 if( !$value ){
1338 $value = $this->parseExpression();
1341 if( !$value ){
1342 if( $isCall ){
1343 $this->Error('could not understand value for named argument');
1344 } else {
1345 $this->restore();
1346 $returner['args'] = array();
1347 return $returner;
1351 $nameLoop = ($name = $val->name);
1352 }elseif( !$isCall && $this->MatchReg('/\\G\.{3}/') ){
1353 $returner['variadic'] = true;
1354 if( $this->MatchChar(";") && !$isSemiColonSeperated ){
1355 $isSemiColonSeperated = true;
1357 if( $isSemiColonSeperated ){
1358 $argsSemiColon[] = array('name'=> $arg->name, 'variadic' => true);
1359 }else{
1360 $argsComma[] = array('name'=> $arg->name, 'variadic' => true);
1362 break;
1363 }elseif( !$isCall ){
1364 $name = $nameLoop = $val->name;
1365 $value = null;
1369 if( $value ){
1370 $expressions[] = $value;
1373 $argsComma[] = array('name'=>$nameLoop, 'value'=>$value );
1375 if( $this->MatchChar(',') ){
1376 continue;
1379 if( $this->MatchChar(';') || $isSemiColonSeperated ){
1381 if( $expressionContainsNamed ){
1382 $this->Error('Cannot mix ; and , as delimiter types');
1385 $isSemiColonSeperated = true;
1387 if( count($expressions) > 1 ){
1388 $value = $this->NewObj1('Less_Tree_Value', $expressions);
1390 $argsSemiColon[] = array('name'=>$name, 'value'=>$value );
1392 $name = null;
1393 $expressions = array();
1394 $expressionContainsNamed = false;
1398 $this->forget();
1399 $returner['args'] = ($isSemiColonSeperated ? $argsSemiColon : $argsComma);
1400 return $returner;
1406 // A Mixin definition, with a list of parameters
1408 // .rounded (@radius: 2px, @color) {
1409 // ...
1410 // }
1412 // Until we have a finer grained state-machine, we have to
1413 // do a look-ahead, to make sure we don't have a mixin call.
1414 // See the `rule` function for more information.
1416 // We start by matching `.rounded (`, and then proceed on to
1417 // the argument list, which has optional default values.
1418 // We store the parameters in `params`, with a `value` key,
1419 // if there is a value, such as in the case of `@radius`.
1421 // Once we've got our params list, and a closing `)`, we parse
1422 // the `{...}` block.
1424 private function parseMixinDefinition(){
1425 $cond = null;
1427 $char = $this->input[$this->pos];
1428 if( ($char !== '.' && $char !== '#') || ($char === '{' && $this->PeekReg('/\\G[^{]*\}/')) ){
1429 return;
1432 $this->save();
1434 $match = $this->MatchReg('/\\G([#.](?:[\w-]|\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/');
1435 if( $match ){
1436 $name = $match[1];
1438 $argInfo = $this->parseMixinArgs( false );
1439 $params = $argInfo['args'];
1440 $variadic = $argInfo['variadic'];
1443 // .mixincall("@{a}");
1444 // looks a bit like a mixin definition..
1445 // also
1446 // .mixincall(@a: {rule: set;});
1447 // so we have to be nice and restore
1448 if( !$this->MatchChar(')') ){
1449 $this->furthest = $this->pos;
1450 $this->restore();
1451 return;
1455 $this->parseComments();
1457 if ($this->MatchReg('/\\Gwhen/')) { // Guard
1458 $cond = $this->expect('parseConditions', 'Expected conditions');
1461 $ruleset = $this->parseBlock();
1463 if( is_array($ruleset) ){
1464 $this->forget();
1465 return $this->NewObj5('Less_Tree_Mixin_Definition', array( $name, $params, $ruleset, $cond, $variadic));
1468 $this->restore();
1469 }else{
1470 $this->forget();
1475 // Entities are the smallest recognized token,
1476 // and can be found inside a rule's value.
1478 private function parseEntity(){
1480 return $this->MatchFuncs( array('parseEntitiesLiteral','parseEntitiesVariable','parseEntitiesUrl','parseEntitiesCall','parseEntitiesKeyword','parseEntitiesJavascript','parseComment') );
1484 // A Rule terminator. Note that we use `peek()` to check for '}',
1485 // because the `block` rule will be expecting it, but we still need to make sure
1486 // it's there, if ';' was ommitted.
1488 private function parseEnd(){
1489 return $this->MatchChar(';') || $this->PeekChar('}');
1493 // IE's alpha function
1495 // alpha(opacity=88)
1497 private function parseAlpha(){
1499 if ( ! $this->MatchReg('/\\G\(opacity=/i')) {
1500 return;
1503 $value = $this->MatchReg('/\\G[0-9]+/');
1504 if( $value ){
1505 $value = $value[0];
1506 }else{
1507 $value = $this->parseEntitiesVariable();
1508 if( !$value ){
1509 return;
1513 $this->expectChar(')');
1514 return $this->NewObj1('Less_Tree_Alpha',$value);
1519 // A Selector Element
1521 // div
1522 // + h1
1523 // #socks
1524 // input[type="text"]
1526 // Elements are the building blocks for Selectors,
1527 // they are made out of a `Combinator` (see combinator rule),
1528 // and an element name, such as a tag a class, or `*`.
1530 private function parseElement(){
1531 $c = $this->parseCombinator();
1532 $index = $this->pos;
1534 $e = $this->match( array('/\\G(?:\d+\.\d+|\d+)%/', '/\\G(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/',
1535 '#*', '#&', 'parseAttribute', '/\\G\([^()@]+\)/', '/\\G[\.#](?=@)/', 'parseEntitiesVariableCurly') );
1537 if( is_null($e) ){
1538 $this->save();
1539 if( $this->MatchChar('(') ){
1540 if( ($v = $this->parseSelector()) && $this->MatchChar(')') ){
1541 $e = $this->NewObj1('Less_Tree_Paren',$v);
1542 $this->forget();
1543 }else{
1544 $this->restore();
1546 }else{
1547 $this->forget();
1551 if( !is_null($e) ){
1552 return $this->NewObj4('Less_Tree_Element',array( $c, $e, $index, $this->env->currentFileInfo));
1557 // Combinators combine elements together, in a Selector.
1559 // Because our parser isn't white-space sensitive, special care
1560 // has to be taken, when parsing the descendant combinator, ` `,
1561 // as it's an empty space. We have to check the previous character
1562 // in the input, to see if it's a ` ` character.
1564 private function parseCombinator(){
1565 $c = $this->input[$this->pos];
1566 if ($c === '>' || $c === '+' || $c === '~' || $c === '|' || $c === '^' ){
1568 $this->pos++;
1569 if( $this->input[$this->pos] === '^' ){
1570 $c = '^^';
1571 $this->pos++;
1574 $this->skipWhitespace(0);
1576 return $c;
1579 if( $this->pos > 0 && $this->isWhitespace(-1) ){
1580 return ' ';
1585 // A CSS selector (see selector below)
1586 // with less extensions e.g. the ability to extend and guard
1588 private function parseLessSelector(){
1589 return $this->parseSelector(true);
1593 // A CSS Selector
1595 // .class > div + h1
1596 // li a:hover
1598 // Selectors are made out of one or more Elements, see above.
1600 private function parseSelector( $isLess = false ){
1601 $elements = array();
1602 $extendList = array();
1603 $condition = null;
1604 $when = false;
1605 $extend = false;
1606 $e = null;
1607 $c = null;
1608 $index = $this->pos;
1610 while( ($isLess && ($extend = $this->parseExtend())) || ($isLess && ($when = $this->MatchReg('/\\Gwhen/') )) || ($e = $this->parseElement()) ){
1611 if( $when ){
1612 $condition = $this->expect('parseConditions', 'expected condition');
1613 }elseif( $condition ){
1614 //error("CSS guard can only be used at the end of selector");
1615 }elseif( $extend ){
1616 $extendList = array_merge($extendList,$extend);
1617 }else{
1618 //if( count($extendList) ){
1619 //error("Extend can only be used at the end of selector");
1621 $c = $this->input[ $this->pos ];
1622 $elements[] = $e;
1623 $e = null;
1626 if( $c === '{' || $c === '}' || $c === ';' || $c === ',' || $c === ')') { break; }
1629 if( $elements ){
1630 return $this->NewObj5('Less_Tree_Selector',array($elements, $extendList, $condition, $index, $this->env->currentFileInfo));
1632 if( $extendList ) {
1633 $this->Error('Extend must be used to extend a selector, it cannot be used on its own');
1637 private function parseTag(){
1638 return ( $tag = $this->MatchReg('/\\G[A-Za-z][A-Za-z-]*[0-9]?/') ) ? $tag : $this->MatchChar('*');
1641 private function parseAttribute(){
1643 $val = null;
1645 if( !$this->MatchChar('[') ){
1646 return;
1649 $key = $this->parseEntitiesVariableCurly();
1650 if( !$key ){
1651 $key = $this->expect('/\\G(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\\\.)+/');
1654 $op = $this->MatchReg('/\\G[|~*$^]?=/');
1655 if( $op ){
1656 $val = $this->match( array('parseEntitiesQuoted','/\\G[0-9]+%/','/\\G[\w-]+/','parseEntitiesVariableCurly') );
1659 $this->expectChar(']');
1661 return $this->NewObj3('Less_Tree_Attribute',array( $key, $op[0], $val));
1665 // The `block` rule is used by `ruleset` and `mixin.definition`.
1666 // It's a wrapper around the `primary` rule, with added `{}`.
1668 private function parseBlock(){
1669 if( $this->MatchChar('{') ){
1670 $content = $this->parsePrimary();
1671 if( $this->MatchChar('}') ){
1672 return $content;
1677 private function parseBlockRuleset(){
1678 $block = $this->parseBlock();
1680 if( $block ){
1681 $block = $this->NewObj2('Less_Tree_Ruleset',array( null, $block));
1684 return $block;
1687 private function parseDetachedRuleset(){
1688 $blockRuleset = $this->parseBlockRuleset();
1689 if( $blockRuleset ){
1690 return $this->NewObj1('Less_Tree_DetachedRuleset',$blockRuleset);
1695 // div, .class, body > p {...}
1697 private function parseRuleset(){
1698 $selectors = array();
1700 $this->save();
1702 while( true ){
1703 $s = $this->parseLessSelector();
1704 if( !$s ){
1705 break;
1707 $selectors[] = $s;
1708 $this->parseComments();
1710 if( $s->condition && count($selectors) > 1 ){
1711 $this->Error('Guards are only currently allowed on a single selector.');
1714 if( !$this->MatchChar(',') ){
1715 break;
1717 if( $s->condition ){
1718 $this->Error('Guards are only currently allowed on a single selector.');
1720 $this->parseComments();
1724 if( $selectors ){
1725 $rules = $this->parseBlock();
1726 if( is_array($rules) ){
1727 $this->forget();
1728 return $this->NewObj2('Less_Tree_Ruleset',array( $selectors, $rules)); //Less_Environment::$strictImports
1732 // Backtrack
1733 $this->furthest = $this->pos;
1734 $this->restore();
1738 * Custom less.php parse function for finding simple name-value css pairs
1739 * ex: width:100px;
1742 private function parseNameValue(){
1744 $index = $this->pos;
1745 $this->save();
1748 //$match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*((?:\'")?[a-zA-Z0-9\-% \.,!]+?(?:\'")?)\s*([;}])/');
1749 $match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*([\'"]?[#a-zA-Z0-9\-%\.,]+?[\'"]?) *(! *important)?\s*([;}])/');
1750 if( $match ){
1752 if( $match[4] == '}' ){
1753 $this->pos = $index + strlen($match[0])-1;
1756 if( $match[3] ){
1757 $match[2] .= ' !important';
1760 return $this->NewObj4('Less_Tree_NameValue',array( $match[1], $match[2], $index, $this->env->currentFileInfo));
1763 $this->restore();
1767 private function parseRule( $tryAnonymous = null ){
1769 $merge = false;
1770 $startOfRule = $this->pos;
1772 $c = $this->input[$this->pos];
1773 if( $c === '.' || $c === '#' || $c === '&' ){
1774 return;
1777 $this->save();
1778 $name = $this->MatchFuncs( array('parseVariable','parseRuleProperty'));
1780 if( $name ){
1782 $isVariable = is_string($name);
1784 $value = null;
1785 if( $isVariable ){
1786 $value = $this->parseDetachedRuleset();
1789 $important = null;
1790 if( !$value ){
1792 // prefer to try to parse first if its a variable or we are compressing
1793 // but always fallback on the other one
1794 //if( !$tryAnonymous && is_string($name) && $name[0] === '@' ){
1795 if( !$tryAnonymous && (Less_Parser::$options['compress'] || $isVariable) ){
1796 $value = $this->MatchFuncs( array('parseValue','parseAnonymousValue'));
1797 }else{
1798 $value = $this->MatchFuncs( array('parseAnonymousValue','parseValue'));
1801 $important = $this->parseImportant();
1803 // a name returned by this.ruleProperty() is always an array of the form:
1804 // [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"]
1805 // where each item is a tree.Keyword or tree.Variable
1806 if( !$isVariable && is_array($name) ){
1807 $nm = array_pop($name);
1808 if( $nm->value ){
1809 $merge = $nm->value;
1815 if( $value && $this->parseEnd() ){
1816 $this->forget();
1817 return $this->NewObj6('Less_Tree_Rule',array( $name, $value, $important, $merge, $startOfRule, $this->env->currentFileInfo));
1818 }else{
1819 $this->furthest = $this->pos;
1820 $this->restore();
1821 if( $value && !$tryAnonymous ){
1822 return $this->parseRule(true);
1825 }else{
1826 $this->forget();
1830 function parseAnonymousValue(){
1832 if( preg_match('/\\G([^@+\/\'"*`(;{}-]*);/',$this->input, $match, 0, $this->pos) ){
1833 $this->pos += strlen($match[1]);
1834 return $this->NewObj1('Less_Tree_Anonymous',$match[1]);
1839 // An @import directive
1841 // @import "lib";
1843 // Depending on our environment, importing is done differently:
1844 // In the browser, it's an XHR request, in Node, it would be a
1845 // file-system operation. The function used for importing is
1846 // stored in `import`, which we pass to the Import constructor.
1848 private function parseImport(){
1850 $this->save();
1852 $dir = $this->MatchReg('/\\G@import?\s+/');
1854 if( $dir ){
1855 $options = $this->parseImportOptions();
1856 $path = $this->MatchFuncs( array('parseEntitiesQuoted','parseEntitiesUrl'));
1858 if( $path ){
1859 $features = $this->parseMediaFeatures();
1860 if( $this->MatchChar(';') ){
1861 if( $features ){
1862 $features = $this->NewObj1('Less_Tree_Value',$features);
1865 $this->forget();
1866 return $this->NewObj5('Less_Tree_Import',array( $path, $features, $options, $this->pos, $this->env->currentFileInfo));
1871 $this->restore();
1874 private function parseImportOptions(){
1876 $options = array();
1878 // list of options, surrounded by parens
1879 if( !$this->MatchChar('(') ){
1880 return $options;
1883 $optionName = $this->parseImportOption();
1884 if( $optionName ){
1885 $value = true;
1886 switch( $optionName ){
1887 case "css":
1888 $optionName = "less";
1889 $value = false;
1890 break;
1891 case "once":
1892 $optionName = "multiple";
1893 $value = false;
1894 break;
1896 $options[$optionName] = $value;
1897 if( !$this->MatchChar(',') ){ break; }
1899 }while( $optionName );
1900 $this->expectChar(')');
1901 return $options;
1904 private function parseImportOption(){
1905 $opt = $this->MatchReg('/\\G(less|css|multiple|once|inline|reference)/');
1906 if( $opt ){
1907 return $opt[1];
1911 private function parseMediaFeature() {
1912 $nodes = array();
1915 $e = $this->MatchFuncs(array('parseEntitiesKeyword','parseEntitiesVariable'));
1916 if( $e ){
1917 $nodes[] = $e;
1918 } elseif ($this->MatchChar('(')) {
1919 $p = $this->parseProperty();
1920 $e = $this->parseValue();
1921 if ($this->MatchChar(')')) {
1922 if ($p && $e) {
1923 $r = $this->NewObj7('Less_Tree_Rule', array( $p, $e, null, null, $this->pos, $this->env->currentFileInfo, true));
1924 $nodes[] = $this->NewObj1('Less_Tree_Paren',$r);
1925 } elseif ($e) {
1926 $nodes[] = $this->NewObj1('Less_Tree_Paren',$e);
1927 } else {
1928 return null;
1930 } else
1931 return null;
1933 } while ($e);
1935 if ($nodes) {
1936 return $this->NewObj1('Less_Tree_Expression',$nodes);
1940 private function parseMediaFeatures() {
1941 $features = array();
1944 $e = $this->parseMediaFeature();
1945 if( $e ){
1946 $features[] = $e;
1947 if (!$this->MatchChar(',')) break;
1948 }else{
1949 $e = $this->parseEntitiesVariable();
1950 if( $e ){
1951 $features[] = $e;
1952 if (!$this->MatchChar(',')) break;
1955 } while ($e);
1957 return $features ? $features : null;
1960 private function parseMedia() {
1961 if( $this->MatchReg('/\\G@media/') ){
1962 $features = $this->parseMediaFeatures();
1963 $rules = $this->parseBlock();
1965 if( is_array($rules) ){
1966 return $this->NewObj4('Less_Tree_Media',array( $rules, $features, $this->pos, $this->env->currentFileInfo));
1973 // A CSS Directive
1975 // @charset "utf-8";
1977 private function parseDirective(){
1979 if( !$this->PeekChar('@') ){
1980 return;
1983 $rules = null;
1984 $index = $this->pos;
1985 $hasBlock = true;
1986 $hasIdentifier = false;
1987 $hasExpression = false;
1988 $hasUnknown = false;
1991 $value = $this->MatchFuncs(array('parseImport','parseMedia'));
1992 if( $value ){
1993 return $value;
1996 $this->save();
1998 $name = $this->MatchReg('/\\G@[a-z-]+/');
2000 if( !$name ) return;
2001 $name = $name[0];
2004 $nonVendorSpecificName = $name;
2005 $pos = strpos($name,'-', 2);
2006 if( $name[1] == '-' && $pos > 0 ){
2007 $nonVendorSpecificName = "@" . substr($name, $pos + 1);
2011 switch( $nonVendorSpecificName ){
2013 case "@font-face":
2014 case "@viewport":
2015 case "@top-left":
2016 case "@top-left-corner":
2017 case "@top-center":
2018 case "@top-right":
2019 case "@top-right-corner":
2020 case "@bottom-left":
2021 case "@bottom-left-corner":
2022 case "@bottom-center":
2023 case "@bottom-right":
2024 case "@bottom-right-corner":
2025 case "@left-top":
2026 case "@left-middle":
2027 case "@left-bottom":
2028 case "@right-top":
2029 case "@right-middle":
2030 case "@right-bottom":
2031 hasBlock = true;
2032 break;
2034 case "@charset":
2035 $hasIdentifier = true;
2036 $hasBlock = false;
2037 break;
2038 case "@namespace":
2039 $hasExpression = true;
2040 $hasBlock = false;
2041 break;
2042 case "@keyframes":
2043 $hasIdentifier = true;
2044 break;
2045 case "@host":
2046 case "@page":
2047 case "@document":
2048 case "@supports":
2049 $hasUnknown = true;
2050 break;
2053 if( $hasIdentifier ){
2054 $value = $this->parseEntity();
2055 if( !$value ){
2056 $this->error("expected " . $name . " identifier");
2058 } else if( $hasExpression ){
2059 $value = $this->parseExpression();
2060 if( !$value ){
2061 $this->error("expected " . $name. " expression");
2063 } else if ($hasUnknown) {
2065 $value = $this->MatchReg('/\\G[^{;]+/');
2066 if( $value ){
2067 $value = $this->NewObj1('Less_Tree_Anonymous',trim($value[0]));
2071 if( $hasBlock ){
2072 $rules = $this->parseBlockRuleset();
2075 if( $rules || (!$hasBlock && $value && $this->MatchChar(';'))) {
2076 $this->forget();
2077 return $this->NewObj5('Less_Tree_Directive',array($name, $value, $rules, $index, $this->env->currentFileInfo));
2080 $this->restore();
2085 // A Value is a comma-delimited list of Expressions
2087 // font-family: Baskerville, Georgia, serif;
2089 // In a Rule, a Value represents everything after the `:`,
2090 // and before the `;`.
2092 private function parseValue(){
2093 $expressions = array();
2096 $e = $this->parseExpression();
2097 if( $e ){
2098 $expressions[] = $e;
2099 if (! $this->MatchChar(',')) {
2100 break;
2103 }while($e);
2105 if( $expressions ){
2106 return $this->NewObj1('Less_Tree_Value',$expressions);
2110 private function parseImportant (){
2111 if( $this->PeekChar('!') && $this->MatchReg('/\\G! *important/') ){
2112 return ' !important';
2116 private function parseSub (){
2118 if( $this->MatchChar('(') ){
2119 $a = $this->parseAddition();
2120 if( $a ){
2121 $this->expectChar(')');
2122 return $this->NewObj2('Less_Tree_Expression',array( array($a), true) ); //instead of $e->parens = true so the value is cached
2129 * Parses multiplication operation
2131 * @return Less_Tree_Operation|null
2133 function parseMultiplication(){
2135 $return = $m = $this->parseOperand();
2136 if( $return ){
2137 while( true ){
2139 $isSpaced = $this->isWhitespace( -1 );
2141 if( $this->PeekReg('/\\G\/[*\/]/') ){
2142 break;
2145 $op = $this->MatchChar('/');
2146 if( !$op ){
2147 $op = $this->MatchChar('*');
2148 if( !$op ){
2149 break;
2153 $a = $this->parseOperand();
2155 if(!$a) { break; }
2157 $m->parensInOp = true;
2158 $a->parensInOp = true;
2159 $return = $this->NewObj3('Less_Tree_Operation',array( $op, array( $return, $a ), $isSpaced) );
2162 return $return;
2168 * Parses an addition operation
2170 * @return Less_Tree_Operation|null
2172 private function parseAddition (){
2174 $return = $m = $this->parseMultiplication();
2175 if( $return ){
2176 while( true ){
2178 $isSpaced = $this->isWhitespace( -1 );
2180 $op = $this->MatchReg('/\\G[-+]\s+/');
2181 if( $op ){
2182 $op = $op[0];
2183 }else{
2184 if( !$isSpaced ){
2185 $op = $this->match(array('#+','#-'));
2187 if( !$op ){
2188 break;
2192 $a = $this->parseMultiplication();
2193 if( !$a ){
2194 break;
2197 $m->parensInOp = true;
2198 $a->parensInOp = true;
2199 $return = $this->NewObj3('Less_Tree_Operation',array($op, array($return, $a), $isSpaced));
2203 return $return;
2208 * Parses the conditions
2210 * @return Less_Tree_Condition|null
2212 private function parseConditions() {
2213 $index = $this->pos;
2214 $return = $a = $this->parseCondition();
2215 if( $a ){
2216 while( true ){
2217 if( !$this->PeekReg('/\\G,\s*(not\s*)?\(/') || !$this->MatchChar(',') ){
2218 break;
2220 $b = $this->parseCondition();
2221 if( !$b ){
2222 break;
2225 $return = $this->NewObj4('Less_Tree_Condition',array('or', $return, $b, $index));
2227 return $return;
2231 private function parseCondition() {
2232 $index = $this->pos;
2233 $negate = false;
2234 $c = null;
2236 if ($this->MatchReg('/\\Gnot/')) $negate = true;
2237 $this->expectChar('(');
2238 $a = $this->MatchFuncs(array('parseAddition','parseEntitiesKeyword','parseEntitiesQuoted'));
2240 if( $a ){
2241 $op = $this->MatchReg('/\\G(?:>=|<=|=<|[<=>])/');
2242 if( $op ){
2243 $b = $this->MatchFuncs(array('parseAddition','parseEntitiesKeyword','parseEntitiesQuoted'));
2244 if( $b ){
2245 $c = $this->NewObj5('Less_Tree_Condition',array($op[0], $a, $b, $index, $negate));
2246 } else {
2247 $this->Error('Unexpected expression');
2249 } else {
2250 $k = $this->NewObj1('Less_Tree_Keyword','true');
2251 $c = $this->NewObj5('Less_Tree_Condition',array('=', $a, $k, $index, $negate));
2253 $this->expectChar(')');
2254 return $this->MatchReg('/\\Gand/') ? $this->NewObj3('Less_Tree_Condition',array('and', $c, $this->parseCondition())) : $c;
2259 * An operand is anything that can be part of an operation,
2260 * such as a Color, or a Variable
2263 private function parseOperand (){
2265 $negate = false;
2266 $offset = $this->pos+1;
2267 if( $offset >= $this->input_len ){
2268 return;
2270 $char = $this->input[$offset];
2271 if( $char === '@' || $char === '(' ){
2272 $negate = $this->MatchChar('-');
2275 $o = $this->MatchFuncs(array('parseSub','parseEntitiesDimension','parseEntitiesColor','parseEntitiesVariable','parseEntitiesCall'));
2277 if( $negate ){
2278 $o->parensInOp = true;
2279 $o = $this->NewObj1('Less_Tree_Negative',$o);
2282 return $o;
2287 * Expressions either represent mathematical operations,
2288 * or white-space delimited Entities.
2290 * 1px solid black
2291 * @var * 2
2293 * @return Less_Tree_Expression|null
2295 private function parseExpression (){
2296 $entities = array();
2299 $e = $this->MatchFuncs(array('parseAddition','parseEntity'));
2300 if( $e ){
2301 $entities[] = $e;
2302 // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here
2303 if( !$this->PeekReg('/\\G\/[\/*]/') ){
2304 $delim = $this->MatchChar('/');
2305 if( $delim ){
2306 $entities[] = $this->NewObj1('Less_Tree_Anonymous',$delim);
2310 }while($e);
2312 if( $entities ){
2313 return $this->NewObj1('Less_Tree_Expression',$entities);
2319 * Parse a property
2320 * eg: 'min-width', 'orientation', etc
2322 * @return string
2324 private function parseProperty (){
2325 $name = $this->MatchReg('/\\G(\*?-?[_a-zA-Z0-9-]+)\s*:/');
2326 if( $name ){
2327 return $name[1];
2333 * Parse a rule property
2334 * eg: 'color', 'width', 'height', etc
2336 * @return string
2338 private function parseRuleProperty(){
2339 $offset = $this->pos;
2340 $name = array();
2341 $index = array();
2342 $length = 0;
2345 $this->rulePropertyMatch('/\\G(\*?)/', $offset, $length, $index, $name );
2346 while( $this->rulePropertyMatch('/\\G((?:[\w-]+)|(?:@\{[\w-]+\}))/', $offset, $length, $index, $name )); // !
2348 if( (count($name) > 1) && $this->rulePropertyMatch('/\\G\s*((?:\+_|\+)?)\s*:/', $offset, $length, $index, $name) ){
2349 // at last, we have the complete match now. move forward,
2350 // convert name particles to tree objects and return:
2351 $this->skipWhitespace($length);
2353 if( $name[0] === '' ){
2354 array_shift($name);
2355 array_shift($index);
2357 foreach($name as $k => $s ){
2358 if( !$s || $s[0] !== '@' ){
2359 $name[$k] = $this->NewObj1('Less_Tree_Keyword',$s);
2360 }else{
2361 $name[$k] = $this->NewObj3('Less_Tree_Variable',array('@' . substr($s,2,-1), $index[$k], $this->env->currentFileInfo));
2364 return $name;
2370 private function rulePropertyMatch( $re, &$offset, &$length, &$index, &$name ){
2371 preg_match($re, $this->input, $a, 0, $offset);
2372 if( $a ){
2373 $index[] = $this->pos + $length;
2374 $length += strlen($a[0]);
2375 $offset += strlen($a[0]);
2376 $name[] = $a[1];
2377 return true;
2381 public function serializeVars( $vars ){
2382 $s = '';
2384 foreach($vars as $name => $value){
2385 $s .= (($name[0] === '@') ? '' : '@') . $name .': '. $value . ((substr($value,-1) === ';') ? '' : ';');
2388 return $s;
2393 * Some versions of php have trouble with method_exists($a,$b) if $a is not an object
2395 * @param string $b
2397 public static function is_method($a,$b){
2398 return is_object($a) && method_exists($a,$b);
2403 * Round numbers similarly to javascript
2404 * eg: 1.499999 to 1 instead of 2
2407 public static function round($i, $precision = 0){
2409 $precision = pow(10,$precision);
2410 $i = $i*$precision;
2412 $ceil = ceil($i);
2413 $floor = floor($i);
2414 if( ($ceil - $i) <= ($i - $floor) ){
2415 return $ceil/$precision;
2416 }else{
2417 return $floor/$precision;
2423 * Create Less_Tree_* objects and optionally generate a cache string
2425 * @return mixed
2427 public function NewObj0($class){
2428 $obj = new $class();
2429 if( Less_Cache::$cache_dir ){
2430 $obj->cache_string = ' new '.$class.'()';
2432 return $obj;
2435 public function NewObj1($class, $arg){
2436 $obj = new $class( $arg );
2437 if( Less_Cache::$cache_dir ){
2438 $obj->cache_string = ' new '.$class.'('.Less_Parser::ArgString($arg).')';
2440 return $obj;
2443 public function NewObj2($class, $args){
2444 $obj = new $class( $args[0], $args[1] );
2445 if( Less_Cache::$cache_dir ){
2446 $this->ObjCache( $obj, $class, $args);
2448 return $obj;
2451 public function NewObj3($class, $args){
2452 $obj = new $class( $args[0], $args[1], $args[2] );
2453 if( Less_Cache::$cache_dir ){
2454 $this->ObjCache( $obj, $class, $args);
2456 return $obj;
2459 public function NewObj4($class, $args){
2460 $obj = new $class( $args[0], $args[1], $args[2], $args[3] );
2461 if( Less_Cache::$cache_dir ){
2462 $this->ObjCache( $obj, $class, $args);
2464 return $obj;
2467 public function NewObj5($class, $args){
2468 $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4] );
2469 if( Less_Cache::$cache_dir ){
2470 $this->ObjCache( $obj, $class, $args);
2472 return $obj;
2475 public function NewObj6($class, $args){
2476 $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5] );
2477 if( Less_Cache::$cache_dir ){
2478 $this->ObjCache( $obj, $class, $args);
2480 return $obj;
2483 public function NewObj7($class, $args){
2484 $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6] );
2485 if( Less_Cache::$cache_dir ){
2486 $this->ObjCache( $obj, $class, $args);
2488 return $obj;
2491 //caching
2492 public function ObjCache($obj, $class, $args=array()){
2493 $obj->cache_string = ' new '.$class.'('. self::ArgCache($args).')';
2496 public function ArgCache($args){
2497 return implode(',',array_map( array('Less_Parser','ArgString'),$args));
2502 * Convert an argument to a string for use in the parser cache
2504 * @return string
2506 public static function ArgString($arg){
2508 $type = gettype($arg);
2510 if( $type === 'object'){
2511 $string = $arg->cache_string;
2512 unset($arg->cache_string);
2513 return $string;
2515 }elseif( $type === 'array' ){
2516 $string = ' Array(';
2517 foreach($arg as $k => $a){
2518 $string .= var_export($k,true).' => '.self::ArgString($a).',';
2520 return $string . ')';
2523 return var_export($arg,true);
2526 public function Error($msg){
2527 throw new Less_Exception_Parser($msg, null, $this->furthest, $this->env->currentFileInfo);
2530 public static function WinPath($path){
2531 return str_replace('\\', '/', $path);