diff options
Diffstat (limited to 'library/vendor/lessphp/lib/Less/Parser.php')
-rw-r--r-- | library/vendor/lessphp/lib/Less/Parser.php | 2676 |
1 files changed, 2676 insertions, 0 deletions
diff --git a/library/vendor/lessphp/lib/Less/Parser.php b/library/vendor/lessphp/lib/Less/Parser.php new file mode 100644 index 0000000..4162e4b --- /dev/null +++ b/library/vendor/lessphp/lib/Less/Parser.php @@ -0,0 +1,2676 @@ +<?php + +require_once dirname( __FILE__ ).'/Cache.php'; + +/** + * Class for parsing and compiling less files into css + * + * @package Less + * @subpackage parser + */ +class Less_Parser { + + /** + * Default parser options + */ + public static $default_options = array( + 'compress' => false, // option - whether to compress + 'strictUnits' => false, // whether units need to evaluate correctly + 'strictMath' => false, // whether math has to be within parenthesis + 'relativeUrls' => true, // option - whether to adjust URL's to be relative + 'urlArgs' => '', // whether to add args into url tokens + 'numPrecision' => 8, + + 'import_dirs' => array(), + 'import_callback' => null, + 'cache_dir' => null, + 'cache_method' => 'php', // false, 'serialize', 'php', 'var_export', 'callback'; + 'cache_callback_get' => null, + 'cache_callback_set' => null, + + 'sourceMap' => false, // whether to output a source map + 'sourceMapBasepath' => null, + 'sourceMapWriteTo' => null, + 'sourceMapURL' => null, + + 'indentation' => ' ', + + 'plugins' => array(), + + ); + + public static $options = array(); + + private $input; // Less input string + private $input_len; // input string length + private $pos; // current index in `input` + private $saveStack = array(); // holds state for backtracking + private $furthest; + private $mb_internal_encoding = ''; // for remember exists value of mbstring.internal_encoding + + /** + * @var Less_Environment + */ + private $env; + + protected $rules = array(); + + private static $imports = array(); + + public static $has_extends = false; + + public static $next_id = 0; + + /** + * Filename to contents of all parsed the files + * + * @var array + */ + public static $contentsMap = array(); + + /** + * @param Less_Environment|array|null $env + */ + public function __construct( $env = null ) { + // Top parser on an import tree must be sure there is one "env" + // which will then be passed around by reference. + if ( $env instanceof Less_Environment ) { + $this->env = $env; + } else { + $this->SetOptions( Less_Parser::$default_options ); + $this->Reset( $env ); + } + + // mbstring.func_overload > 1 bugfix + // The encoding value must be set for each source file, + // therefore, to conserve resources and improve the speed of this design is taken here + if ( ini_get( 'mbstring.func_overload' ) ) { + $this->mb_internal_encoding = ini_get( 'mbstring.internal_encoding' ); + @ini_set( 'mbstring.internal_encoding', 'ascii' ); + } + + } + + /** + * Reset the parser state completely + */ + public function Reset( $options = null ) { + $this->rules = array(); + self::$imports = array(); + self::$has_extends = false; + self::$imports = array(); + self::$contentsMap = array(); + + $this->env = new Less_Environment(); + + // set new options + if ( is_array( $options ) ) { + $this->SetOptions( Less_Parser::$default_options ); + $this->SetOptions( $options ); + } + + $this->env->Init(); + } + + /** + * Set one or more compiler options + * options: import_dirs, cache_dir, cache_method + */ + public function SetOptions( $options ) { + foreach ( $options as $option => $value ) { + $this->SetOption( $option, $value ); + } + } + + /** + * Set one compiler option + */ + public function SetOption( $option, $value ) { + switch ( $option ) { + + case 'import_dirs': + $this->SetImportDirs( $value ); + return; + + case 'cache_dir': + if ( is_string( $value ) ) { + Less_Cache::SetCacheDir( $value ); + Less_Cache::CheckCacheDir(); + } + return; + } + + Less_Parser::$options[$option] = $value; + } + + /** + * Registers a new custom function + * + * @param string $name function name + * @param callable $callback callback + */ + public function registerFunction( $name, $callback ) { + $this->env->functions[$name] = $callback; + } + + /** + * Removed an already registered function + * + * @param string $name function name + */ + public function unregisterFunction( $name ) { + if ( isset( $this->env->functions[$name] ) ) + unset( $this->env->functions[$name] ); + } + + /** + * Get the current css buffer + * + * @return string + */ + public function getCss() { + $precision = ini_get( 'precision' ); + @ini_set( 'precision', '16' ); + $locale = setlocale( LC_NUMERIC, 0 ); + setlocale( LC_NUMERIC, "C" ); + + try { + $root = new Less_Tree_Ruleset( array(), $this->rules ); + $root->root = true; + $root->firstRoot = true; + + $this->PreVisitors( $root ); + + self::$has_extends = false; + $evaldRoot = $root->compile( $this->env ); + + $this->PostVisitors( $evaldRoot ); + + if ( Less_Parser::$options['sourceMap'] ) { + $generator = new Less_SourceMap_Generator( $evaldRoot, Less_Parser::$contentsMap, Less_Parser::$options ); + // will also save file + // FIXME: should happen somewhere else? + $css = $generator->generateCSS(); + } else { + $css = $evaldRoot->toCSS(); + } + + if ( Less_Parser::$options['compress'] ) { + $css = preg_replace( '/(^(\s)+)|((\s)+$)/', '', $css ); + } + + } catch ( Exception $exc ) { + // Intentional fall-through so we can reset environment + } + + // reset php settings + @ini_set( 'precision', $precision ); + setlocale( LC_NUMERIC, $locale ); + + // If you previously defined $this->mb_internal_encoding + // is required to return the encoding as it was before + if ( $this->mb_internal_encoding != '' ) { + @ini_set( "mbstring.internal_encoding", $this->mb_internal_encoding ); + $this->mb_internal_encoding = ''; + } + + // Rethrow exception after we handled resetting the environment + if ( !empty( $exc ) ) { + throw $exc; + } + + return $css; + } + + public function findValueOf( $varName ) { + foreach ( $this->rules as $rule ) { + if ( isset( $rule->variable ) && ( $rule->variable == true ) && ( str_replace( "@", "", $rule->name ) == $varName ) ) { + return $this->getVariableValue( $rule ); + } + } + return null; + } + + /** + * + * this function gets the private rules variable and returns an array of the found variables + * it uses a helper method getVariableValue() that contains the logic ot fetch the value from the rule object + * + * @return array + */ + public function getVariables() { + $variables = array(); + + $not_variable_type = array( + 'Comment', // this include less comments ( // ) and css comments (/* */) + 'Import', // do not search variables in included files @import + 'Ruleset', // selectors (.someclass, #someid, …) + 'Operation', // + ); + + // @TODO run compilation if not runned yet + foreach ( $this->rules as $key => $rule ) { + if ( in_array( $rule->type, $not_variable_type ) ) { + continue; + } + + // Note: it seems rule->type is always Rule when variable = true + if ( $rule->type == 'Rule' && $rule->variable ) { + $variables[$rule->name] = $this->getVariableValue( $rule ); + } else { + if ( $rule->type == 'Comment' ) { + $variables[] = $this->getVariableValue( $rule ); + } + } + } + return $variables; + } + + public function findVarByName( $var_name ) { + foreach ( $this->rules as $rule ) { + if ( isset( $rule->variable ) && ( $rule->variable == true ) ) { + if ( $rule->name == $var_name ) { + return $this->getVariableValue( $rule ); + } + } + } + return null; + } + + /** + * This method gets the value of the less variable from the rules object. + * Since the objects vary here we add the logic for extracting the css/less value. + * + * @param $var + * @return string + */ + private function getVariableValue( $var ) { + if ( !is_a( $var, 'Less_Tree' ) ) { + throw new Exception( 'var is not a Less_Tree object' ); + } + + switch ( $var->type ) { + case 'Color': + return $this->rgb2html( $var->rgb ); + case 'Unit': + return $var->value. $var->unit->numerator[0]; + case 'Variable': + return $this->findVarByName( $var->name ); + case 'Keyword': + return $var->value; + case 'Url': + // Based on Less_Tree_Url::genCSS() + // Recurse to serialize the Less_Tree_Quoted value + return 'url(' . $this->getVariableValue( $var->value ) . ')'; + case 'Rule': + return $this->getVariableValue( $var->value ); + case 'Value': + $value = ''; + foreach ( $var->value as $sub_value ) { + $value .= $this->getVariableValue( $sub_value ).' '; + } + return $value; + case 'Quoted': + return $var->quote.$var->value.$var->quote; + case 'Dimension': + $value = $var->value; + if ( $var->unit && $var->unit->numerator ) { + $value .= $var->unit->numerator[0]; + } + return $value; + case 'Expression': + $value = ""; + foreach ( $var->value as $item ) { + $value .= $this->getVariableValue( $item )." "; + } + return $value; + case 'Operation': + throw new Exception( 'getVariables() require Less to be compiled. please use $parser->getCss() before calling getVariables()' ); + case 'Comment': + case 'Import': + case 'Ruleset': + default: + throw new Exception( "type missing in switch/case getVariableValue for ".$var->type ); + } + } + + private function rgb2html( $r, $g = -1, $b = -1 ) { + if ( is_array( $r ) && sizeof( $r ) == 3 ) + list( $r, $g, $b ) = $r; + + $r = intval( $r ); $g = intval( $g ); + $b = intval( $b ); + + $r = dechex( $r < 0 ? 0 : ( $r > 255 ? 255 : $r ) ); + $g = dechex( $g < 0 ? 0 : ( $g > 255 ? 255 : $g ) ); + $b = dechex( $b < 0 ? 0 : ( $b > 255 ? 255 : $b ) ); + + $color = ( strlen( $r ) < 2 ? '0' : '' ).$r; + $color .= ( strlen( $g ) < 2 ? '0' : '' ).$g; + $color .= ( strlen( $b ) < 2 ? '0' : '' ).$b; + return '#'.$color; + } + + /** + * Run pre-compile visitors + */ + private function PreVisitors( $root ) { + if ( Less_Parser::$options['plugins'] ) { + foreach ( Less_Parser::$options['plugins'] as $plugin ) { + if ( !empty( $plugin->isPreEvalVisitor ) ) { + $plugin->run( $root ); + } + } + } + } + + /** + * Run post-compile visitors + */ + private function PostVisitors( $evaldRoot ) { + $visitors = array(); + $visitors[] = new Less_Visitor_joinSelector(); + if ( self::$has_extends ) { + $visitors[] = new Less_Visitor_processExtends(); + } + $visitors[] = new Less_Visitor_toCSS(); + + if ( Less_Parser::$options['plugins'] ) { + foreach ( Less_Parser::$options['plugins'] as $plugin ) { + if ( property_exists( $plugin, 'isPreEvalVisitor' ) && $plugin->isPreEvalVisitor ) { + continue; + } + + if ( property_exists( $plugin, 'isPreVisitor' ) && $plugin->isPreVisitor ) { + array_unshift( $visitors, $plugin ); + } else { + $visitors[] = $plugin; + } + } + } + + for ( $i = 0; $i < count( $visitors ); $i++ ) { + $visitors[$i]->run( $evaldRoot ); + } + + } + + /** + * Parse a Less string into css + * + * @param string $str The string to convert + * @param string|null $file_uri The url of the file + * @return Less_Tree_Ruleset|Less_Parser + */ + public function parse( $str, $file_uri = null ) { + if ( !$file_uri ) { + $uri_root = ''; + $filename = 'anonymous-file-'.Less_Parser::$next_id++.'.less'; + } else { + $file_uri = self::WinPath( $file_uri ); + $filename = $file_uri; + $uri_root = dirname( $file_uri ); + } + + $previousFileInfo = $this->env->currentFileInfo; + $uri_root = self::WinPath( $uri_root ); + $this->SetFileInfo( $filename, $uri_root ); + + $this->input = $str; + $this->_parse(); + + if ( $previousFileInfo ) { + $this->env->currentFileInfo = $previousFileInfo; + } + + return $this; + } + + /** + * Parse a Less string from a given file + * + * @throws Less_Exception_Parser + * @param string $filename The file to parse + * @param string $uri_root The url of the file + * @param bool $returnRoot Indicates whether the return value should be a css string a root node + * @return Less_Tree_Ruleset|Less_Parser + */ + public function parseFile( $filename, $uri_root = '', $returnRoot = false ) { + if ( !file_exists( $filename ) ) { + $this->Error( sprintf( 'File `%s` not found.', $filename ) ); + } + + // fix uri_root? + // Instead of The mixture of file path for the first argument and directory path for the second argument has bee + if ( !$returnRoot && !empty( $uri_root ) && basename( $uri_root ) == basename( $filename ) ) { + $uri_root = dirname( $uri_root ); + } + + $previousFileInfo = $this->env->currentFileInfo; + + if ( $filename ) { + $filename = self::AbsPath( $filename, true ); + } + $uri_root = self::WinPath( $uri_root ); + + $this->SetFileInfo( $filename, $uri_root ); + + self::AddParsedFile( $filename ); + + if ( $returnRoot ) { + $rules = $this->GetRules( $filename ); + $return = new Less_Tree_Ruleset( array(), $rules ); + } else { + $this->_parse( $filename ); + $return = $this; + } + + if ( $previousFileInfo ) { + $this->env->currentFileInfo = $previousFileInfo; + } + + return $return; + } + + /** + * Allows a user to set variables values + * @param array $vars + * @return Less_Parser + */ + public function ModifyVars( $vars ) { + $this->input = Less_Parser::serializeVars( $vars ); + $this->_parse(); + + return $this; + } + + /** + * @param string $filename + */ + public function SetFileInfo( $filename, $uri_root = '' ) { + $filename = Less_Environment::normalizePath( $filename ); + $dirname = preg_replace( '/[^\/\\\\]*$/', '', $filename ); + + if ( !empty( $uri_root ) ) { + $uri_root = rtrim( $uri_root, '/' ).'/'; + } + + $currentFileInfo = array(); + + // entry info + if ( isset( $this->env->currentFileInfo ) ) { + $currentFileInfo['entryPath'] = $this->env->currentFileInfo['entryPath']; + $currentFileInfo['entryUri'] = $this->env->currentFileInfo['entryUri']; + $currentFileInfo['rootpath'] = $this->env->currentFileInfo['rootpath']; + + } else { + $currentFileInfo['entryPath'] = $dirname; + $currentFileInfo['entryUri'] = $uri_root; + $currentFileInfo['rootpath'] = $dirname; + } + + $currentFileInfo['currentDirectory'] = $dirname; + $currentFileInfo['currentUri'] = $uri_root.basename( $filename ); + $currentFileInfo['filename'] = $filename; + $currentFileInfo['uri_root'] = $uri_root; + + // inherit reference + if ( isset( $this->env->currentFileInfo['reference'] ) && $this->env->currentFileInfo['reference'] ) { + $currentFileInfo['reference'] = true; + } + + $this->env->currentFileInfo = $currentFileInfo; + } + + /** + * @deprecated 1.5.1.2 + */ + public function SetCacheDir( $dir ) { + if ( !file_exists( $dir ) ) { + if ( mkdir( $dir ) ) { + return true; + } + throw new Less_Exception_Parser( 'Less.php cache directory couldn\'t be created: '.$dir ); + + } elseif ( !is_dir( $dir ) ) { + throw new Less_Exception_Parser( 'Less.php cache directory doesn\'t exist: '.$dir ); + + } elseif ( !is_writable( $dir ) ) { + throw new Less_Exception_Parser( 'Less.php cache directory isn\'t writable: '.$dir ); + + } else { + $dir = self::WinPath( $dir ); + Less_Cache::$cache_dir = rtrim( $dir, '/' ).'/'; + return true; + } + } + + /** + * Set a list of directories or callbacks the parser should use for determining import paths + * + * @param array $dirs + */ + public function SetImportDirs( $dirs ) { + Less_Parser::$options['import_dirs'] = array(); + + foreach ( $dirs as $path => $uri_root ) { + + $path = self::WinPath( $path ); + if ( !empty( $path ) ) { + $path = rtrim( $path, '/' ).'/'; + } + + if ( !is_callable( $uri_root ) ) { + $uri_root = self::WinPath( $uri_root ); + if ( !empty( $uri_root ) ) { + $uri_root = rtrim( $uri_root, '/' ).'/'; + } + } + + Less_Parser::$options['import_dirs'][$path] = $uri_root; + } + } + + /** + * @param string $file_path + */ + private function _parse( $file_path = null ) { + $this->rules = array_merge( $this->rules, $this->GetRules( $file_path ) ); + } + + /** + * Return the results of parsePrimary for $file_path + * Use cache and save cached results if possible + * + * @param string|null $file_path + */ + private function GetRules( $file_path ) { + $this->SetInput( $file_path ); + + $cache_file = $this->CacheFile( $file_path ); + if ( $cache_file ) { + if ( Less_Parser::$options['cache_method'] == 'callback' ) { + if ( is_callable( Less_Parser::$options['cache_callback_get'] ) ) { + $cache = call_user_func_array( + Less_Parser::$options['cache_callback_get'], + array( $this, $file_path, $cache_file ) + ); + + if ( $cache ) { + $this->UnsetInput(); + return $cache; + } + } + + } elseif ( file_exists( $cache_file ) ) { + switch ( Less_Parser::$options['cache_method'] ) { + + // Using serialize + // Faster but uses more memory + case 'serialize': + $cache = unserialize( file_get_contents( $cache_file ) ); + if ( $cache ) { + touch( $cache_file ); + $this->UnsetInput(); + return $cache; + } + break; + + // Using generated php code + case 'var_export': + case 'php': + $this->UnsetInput(); + return include $cache_file; + } + } + } + + $rules = $this->parsePrimary(); + + if ( $this->pos < $this->input_len ) { + throw new Less_Exception_Chunk( $this->input, null, $this->furthest, $this->env->currentFileInfo ); + } + + $this->UnsetInput(); + + // save the cache + if ( $cache_file ) { + if ( Less_Parser::$options['cache_method'] == 'callback' ) { + if ( is_callable( Less_Parser::$options['cache_callback_set'] ) ) { + call_user_func_array( + Less_Parser::$options['cache_callback_set'], + array( $this, $file_path, $cache_file, $rules ) + ); + } + + } else { + // msg('write cache file'); + switch ( Less_Parser::$options['cache_method'] ) { + case 'serialize': + file_put_contents( $cache_file, serialize( $rules ) ); + break; + case 'php': + file_put_contents( $cache_file, '<?php return '.self::ArgString( $rules ).'; ?>' ); + break; + case 'var_export': + // Requires __set_state() + file_put_contents( $cache_file, '<?php return '.var_export( $rules, true ).'; ?>' ); + break; + } + + Less_Cache::CleanCache(); + } + } + + return $rules; + } + + /** + * Set up the input buffer + */ + public function SetInput( $file_path ) { + if ( $file_path ) { + $this->input = file_get_contents( $file_path ); + } + + $this->pos = $this->furthest = 0; + + // Remove potential UTF Byte Order Mark + $this->input = preg_replace( '/\\G\xEF\xBB\xBF/', '', $this->input ); + $this->input_len = strlen( $this->input ); + + if ( Less_Parser::$options['sourceMap'] && $this->env->currentFileInfo ) { + $uri = $this->env->currentFileInfo['currentUri']; + Less_Parser::$contentsMap[$uri] = $this->input; + } + + } + + /** + * Free up some memory + */ + public function UnsetInput() { + $this->input = $this->pos = $this->input_len = $this->furthest = null; + $this->saveStack = array(); + } + + public function CacheFile( $file_path ) { + if ( $file_path && $this->CacheEnabled() ) { + + $env = get_object_vars( $this->env ); + unset( $env['frames'] ); + + $parts = array(); + $parts[] = $file_path; + $parts[] = filesize( $file_path ); + $parts[] = filemtime( $file_path ); + $parts[] = $env; + $parts[] = Less_Version::cache_version; + $parts[] = Less_Parser::$options['cache_method']; + return Less_Cache::$cache_dir . Less_Cache::$prefix . base_convert( sha1( json_encode( $parts ) ), 16, 36 ) . '.lesscache'; + } + } + + static function AddParsedFile( $file ) { + self::$imports[] = $file; + } + + static function AllParsedFiles() { + return self::$imports; + } + + /** + * @param string $file + */ + static function FileParsed( $file ) { + return in_array( $file, self::$imports ); + } + + function save() { + $this->saveStack[] = $this->pos; + } + + private function restore() { + $this->pos = array_pop( $this->saveStack ); + } + + private function forget() { + array_pop( $this->saveStack ); + } + + /** + * Determine if the character at the specified offset from the current position is a white space. + * + * @param int $offset + * + * @return bool + */ + private function isWhitespace( $offset = 0 ) { + return strpos( " \t\n\r\v\f", $this->input[$this->pos + $offset] ) !== false; + } + + /** + * Parse from a token, regexp or string, and move forward if match + * + * @param array $toks + * @return array + */ + private function match( $toks ) { + // The match is confirmed, add the match length to `this::pos`, + // and consume any extra white-space characters (' ' || '\n') + // which come after that. The reason for this is that LeSS's + // grammar is mostly white-space insensitive. + // + + foreach ( $toks as $tok ) { + + $char = $tok[0]; + + if ( $char === '/' ) { + $match = $this->MatchReg( $tok ); + + if ( $match ) { + return count( $match ) === 1 ? $match[0] : $match; + } + + } elseif ( $char === '#' ) { + $match = $this->MatchChar( $tok[1] ); + + } else { + // Non-terminal, match using a function call + $match = $this->$tok(); + + } + + if ( $match ) { + return $match; + } + } + } + + /** + * @param string[] $toks + * + * @return string + */ + private function MatchFuncs( $toks ) { + if ( $this->pos < $this->input_len ) { + foreach ( $toks as $tok ) { + $match = $this->$tok(); + if ( $match ) { + return $match; + } + } + } + + } + + // Match a single character in the input, + private function MatchChar( $tok ) { + if ( ( $this->pos < $this->input_len ) && ( $this->input[$this->pos] === $tok ) ) { + $this->skipWhitespace( 1 ); + return $tok; + } + } + + // Match a regexp from the current start point + private function MatchReg( $tok ) { + if ( preg_match( $tok, $this->input, $match, 0, $this->pos ) ) { + $this->skipWhitespace( strlen( $match[0] ) ); + return $match; + } + } + + /** + * Same as match(), but don't change the state of the parser, + * just return the match. + * + * @param string $tok + * @return integer + */ + public function PeekReg( $tok ) { + return preg_match( $tok, $this->input, $match, 0, $this->pos ); + } + + /** + * @param string $tok + */ + public function PeekChar( $tok ) { + // return ($this->input[$this->pos] === $tok ); + return ( $this->pos < $this->input_len ) && ( $this->input[$this->pos] === $tok ); + } + + /** + * @param integer $length + */ + public function skipWhitespace( $length ) { + $this->pos += $length; + + for ( ; $this->pos < $this->input_len; $this->pos++ ) { + $c = $this->input[$this->pos]; + + if ( ( $c !== "\n" ) && ( $c !== "\r" ) && ( $c !== "\t" ) && ( $c !== ' ' ) ) { + break; + } + } + } + + /** + * @param string $tok + * @param string|null $msg + */ + public function expect( $tok, $msg = NULL ) { + $result = $this->match( array( $tok ) ); + if ( !$result ) { + $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg ); + } else { + return $result; + } + } + + /** + * @param string $tok + */ + public function expectChar( $tok, $msg = null ) { + $result = $this->MatchChar( $tok ); + if ( !$result ) { + $msg = $msg ? $msg : "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'"; + $this->Error( $msg ); + } else { + return $result; + } + } + + // + // Here in, the parsing rules/functions + // + // The basic structure of the syntax tree generated is as follows: + // + // Ruleset -> Rule -> Value -> Expression -> Entity + // + // Here's some LESS code: + // + // .class { + // color: #fff; + // border: 1px solid #000; + // width: @w + 4px; + // > .child {...} + // } + // + // And here's what the parse tree might look like: + // + // Ruleset (Selector '.class', [ + // Rule ("color", Value ([Expression [Color #fff]])) + // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]])) + // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]])) + // Ruleset (Selector [Element '>', '.child'], [...]) + // ]) + // + // In general, most rules will try to parse a token with the `$()` function, and if the return + // value is truly, will return a new node, of the relevant type. Sometimes, we need to check + // first, before parsing, that's when we use `peek()`. + // + + // + // The `primary` rule is the *entry* and *exit* point of the parser. + // The rules here can appear at any level of the parse tree. + // + // The recursive nature of the grammar is an interplay between the `block` + // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule, + // as represented by this simplified grammar: + // + // primary → (ruleset | rule)+ + // ruleset → selector+ block + // block → '{' primary '}' + // + // Only at one point is the primary rule not called from the + // block rule: at the root level. + // + private function parsePrimary() { + $root = array(); + + while ( true ) { + + if ( $this->pos >= $this->input_len ) { + break; + } + + $node = $this->parseExtend( true ); + if ( $node ) { + $root = array_merge( $root, $node ); + continue; + } + + // $node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseDirective')); + $node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseNameValue', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseRulesetCall', 'parseDirective' ) ); + + if ( $node ) { + $root[] = $node; + } elseif ( !$this->MatchReg( '/\\G[\s\n;]+/' ) ) { + break; + } + + if ( $this->PeekChar( '}' ) ) { + break; + } + } + + return $root; + } + + // We create a Comment node for CSS comments `/* */`, + // but keep the LeSS comments `//` silent, by just skipping + // over them. + private function parseComment() { + if ( $this->input[$this->pos] !== '/' ) { + return; + } + + if ( $this->input[$this->pos + 1] === '/' ) { + $match = $this->MatchReg( '/\\G\/\/.*/' ); + return $this->NewObj4( 'Less_Tree_Comment', array( $match[0], true, $this->pos, $this->env->currentFileInfo ) ); + } + + // $comment = $this->MatchReg('/\\G\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/'); + $comment = $this->MatchReg( '/\\G\/\*(?s).*?\*+\/\n?/' );// not the same as less.js to prevent fatal errors + if ( $comment ) { + return $this->NewObj4( 'Less_Tree_Comment', array( $comment[0], false, $this->pos, $this->env->currentFileInfo ) ); + } + } + + private function parseComments() { + $comments = array(); + + while ( $this->pos < $this->input_len ) { + $comment = $this->parseComment(); + if ( !$comment ) { + break; + } + + $comments[] = $comment; + } + + return $comments; + } + + // + // A string, which supports escaping " and ' + // + // "milky way" 'he\'s the one!' + // + private function parseEntitiesQuoted() { + $j = $this->pos; + $e = false; + $index = $this->pos; + + if ( $this->input[$this->pos] === '~' ) { + $j++; + $e = true; // Escaped strings + } + + $char = $this->input[$j]; + if ( $char !== '"' && $char !== "'" ) { + return; + } + + if ( $e ) { + $this->MatchChar( '~' ); + } + + $matched = $this->MatchQuoted( $char, $j + 1 ); + if ( $matched === false ) { + return; + } + + $quoted = $char.$matched.$char; + return $this->NewObj5( 'Less_Tree_Quoted', array( $quoted, $matched, $e, $index, $this->env->currentFileInfo ) ); + } + + /** + * When PCRE JIT is enabled in php, regular expressions don't work for matching quoted strings + * + * $regex = '/\\G\'((?:[^\'\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)\'/'; + * $regex = '/\\G"((?:[^"\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)"/'; + * + */ + private function MatchQuoted( $quote_char, $i ) { + $matched = ''; + while ( $i < $this->input_len ) { + $c = $this->input[$i]; + + // escaped character + if ( $c === '\\' ) { + $matched .= $c . $this->input[$i + 1]; + $i += 2; + continue; + } + + if ( $c === $quote_char ) { + $this->pos = $i + 1; + $this->skipWhitespace( 0 ); + return $matched; + } + + if ( $c === "\r" || $c === "\n" ) { + return false; + } + + $i++; + $matched .= $c; + } + + return false; + } + + // + // A catch-all word, such as: + // + // black border-collapse + // + private function parseEntitiesKeyword() { + // $k = $this->MatchReg('/\\G[_A-Za-z-][_A-Za-z0-9-]*/'); + $k = $this->MatchReg( '/\\G%|\\G[_A-Za-z-][_A-Za-z0-9-]*/' ); + if ( $k ) { + $k = $k[0]; + $color = $this->fromKeyword( $k ); + if ( $color ) { + return $color; + } + return $this->NewObj1( 'Less_Tree_Keyword', $k ); + } + } + + // duplicate of Less_Tree_Color::FromKeyword + private function FromKeyword( $keyword ) { + $keyword = strtolower( $keyword ); + + if ( Less_Colors::hasOwnProperty( $keyword ) ) { + // detect named color + return $this->NewObj1( 'Less_Tree_Color', substr( Less_Colors::color( $keyword ), 1 ) ); + } + + if ( $keyword === 'transparent' ) { + return $this->NewObj3( 'Less_Tree_Color', array( array( 0, 0, 0 ), 0, true ) ); + } + } + + // + // A function call + // + // rgb(255, 0, 255) + // + // We also try to catch IE's `alpha()`, but let the `alpha` parser + // deal with the details. + // + // The arguments are parsed with the `entities.arguments` parser. + // + private function parseEntitiesCall() { + $index = $this->pos; + + if ( !preg_match( '/\\G([\w-]+|%|progid:[\w\.]+)\(/', $this->input, $name, 0, $this->pos ) ) { + return; + } + $name = $name[1]; + $nameLC = strtolower( $name ); + + if ( $nameLC === 'url' ) { + return null; + } + + $this->pos += strlen( $name ); + + if ( $nameLC === 'alpha' ) { + $alpha_ret = $this->parseAlpha(); + if ( $alpha_ret ) { + return $alpha_ret; + } + } + + $this->MatchChar( '(' ); // Parse the '(' and consume whitespace. + + $args = $this->parseEntitiesArguments(); + + if ( !$this->MatchChar( ')' ) ) { + return; + } + + if ( $name ) { + return $this->NewObj4( 'Less_Tree_Call', array( $name, $args, $index, $this->env->currentFileInfo ) ); + } + } + + /** + * Parse a list of arguments + * + * @return array + */ + private function parseEntitiesArguments() { + $args = array(); + while ( true ) { + $arg = $this->MatchFuncs( array( 'parseEntitiesAssignment','parseExpression' ) ); + if ( !$arg ) { + break; + } + + $args[] = $arg; + if ( !$this->MatchChar( ',' ) ) { + break; + } + } + return $args; + } + + private function parseEntitiesLiteral() { + return $this->MatchFuncs( array( 'parseEntitiesDimension','parseEntitiesColor','parseEntitiesQuoted','parseUnicodeDescriptor' ) ); + } + + // Assignments are argument entities for calls. + // They are present in ie filter properties as shown below. + // + // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* ) + // + private function parseEntitiesAssignment() { + $key = $this->MatchReg( '/\\G\w+(?=\s?=)/' ); + if ( !$key ) { + return; + } + + if ( !$this->MatchChar( '=' ) ) { + return; + } + + $value = $this->parseEntity(); + if ( $value ) { + return $this->NewObj2( 'Less_Tree_Assignment', array( $key[0], $value ) ); + } + } + + // + // Parse url() tokens + // + // We use a specific rule for urls, because they don't really behave like + // standard function calls. The difference is that the argument doesn't have + // to be enclosed within a string, so it can't be parsed as an Expression. + // + private function parseEntitiesUrl() { + if ( $this->input[$this->pos] !== 'u' || !$this->matchReg( '/\\Gurl\(/' ) ) { + return; + } + + $value = $this->match( array( 'parseEntitiesQuoted','parseEntitiesVariable','/\\Gdata\:.*?[^\)]+/','/\\G(?:(?:\\\\[\(\)\'"])|[^\(\)\'"])+/' ) ); + if ( !$value ) { + $value = ''; + } + + $this->expectChar( ')' ); + + if ( isset( $value->value ) || $value instanceof Less_Tree_Variable ) { + return $this->NewObj2( 'Less_Tree_Url', array( $value, $this->env->currentFileInfo ) ); + } + + return $this->NewObj2( 'Less_Tree_Url', array( $this->NewObj1( 'Less_Tree_Anonymous', $value ), $this->env->currentFileInfo ) ); + } + + // + // A Variable entity, such as `@fink`, in + // + // width: @fink + 2px + // + // We use a different parser for variable definitions, + // see `parsers.variable`. + // + private function parseEntitiesVariable() { + $index = $this->pos; + if ( $this->PeekChar( '@' ) && ( $name = $this->MatchReg( '/\\G@@?[\w-]+/' ) ) ) { + return $this->NewObj3( 'Less_Tree_Variable', array( $name[0], $index, $this->env->currentFileInfo ) ); + } + } + + // A variable entity using the protective {} e.g. @{var} + private function parseEntitiesVariableCurly() { + $index = $this->pos; + + if ( $this->input_len > ( $this->pos + 1 ) && $this->input[$this->pos] === '@' && ( $curly = $this->MatchReg( '/\\G@\{([\w-]+)\}/' ) ) ) { + return $this->NewObj3( 'Less_Tree_Variable', array( '@'.$curly[1], $index, $this->env->currentFileInfo ) ); + } + } + + // + // A Hexadecimal color + // + // #4F3C2F + // + // `rgb` and `hsl` colors are parsed through the `entities.call` parser. + // + private function parseEntitiesColor() { + if ( $this->PeekChar( '#' ) && ( $rgb = $this->MatchReg( '/\\G#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/' ) ) ) { + return $this->NewObj1( 'Less_Tree_Color', $rgb[1] ); + } + } + + // + // A Dimension, that is, a number and a unit + // + // 0.5em 95% + // + private function parseEntitiesDimension() { + $c = @ord( $this->input[$this->pos] ); + + // Is the first char of the dimension 0-9, '.', '+' or '-' + if ( ( $c > 57 || $c < 43 ) || $c === 47 || $c == 44 ) { + return; + } + + $value = $this->MatchReg( '/\\G([+-]?\d*\.?\d+)(%|[a-z]+)?/' ); + if ( $value ) { + + if ( isset( $value[2] ) ) { + return $this->NewObj2( 'Less_Tree_Dimension', array( $value[1],$value[2] ) ); + } + return $this->NewObj1( 'Less_Tree_Dimension', $value[1] ); + } + } + + // + // A unicode descriptor, as is used in unicode-range + // + // U+0?? or U+00A1-00A9 + // + function parseUnicodeDescriptor() { + $ud = $this->MatchReg( '/\\G(U\+[0-9a-fA-F?]+)(\-[0-9a-fA-F?]+)?/' ); + if ( $ud ) { + return $this->NewObj1( 'Less_Tree_UnicodeDescriptor', $ud[0] ); + } + } + + // + // JavaScript code to be evaluated + // + // `window.location.href` + // + private function parseEntitiesJavascript() { + $e = false; + $j = $this->pos; + if ( $this->input[$j] === '~' ) { + $j++; + $e = true; + } + if ( $this->input[$j] !== '`' ) { + return; + } + if ( $e ) { + $this->MatchChar( '~' ); + } + $str = $this->MatchReg( '/\\G`([^`]*)`/' ); + if ( $str ) { + return $this->NewObj3( 'Less_Tree_Javascript', array( $str[1], $this->pos, $e ) ); + } + } + + // + // The variable part of a variable definition. Used in the `rule` parser + // + // @fink: + // + private function parseVariable() { + if ( $this->PeekChar( '@' ) && ( $name = $this->MatchReg( '/\\G(@[\w-]+)\s*:/' ) ) ) { + return $name[1]; + } + } + + // + // The variable part of a variable definition. Used in the `rule` parser + // + // @fink(); + // + private function parseRulesetCall() { + if ( $this->input[$this->pos] === '@' && ( $name = $this->MatchReg( '/\\G(@[\w-]+)\s*\(\s*\)\s*;/' ) ) ) { + return $this->NewObj1( 'Less_Tree_RulesetCall', $name[1] ); + } + } + + // + // extend syntax - used to extend selectors + // + function parseExtend( $isRule = false ) { + $index = $this->pos; + $extendList = array(); + + if ( !$this->MatchReg( $isRule ? '/\\G&:extend\(/' : '/\\G:extend\(/' ) ) { return; + } + + do{ + $option = null; + $elements = array(); + while ( true ) { + $option = $this->MatchReg( '/\\G(all)(?=\s*(\)|,))/' ); + if ( $option ) { break; + } + $e = $this->parseElement(); + if ( !$e ) { break; + } + $elements[] = $e; + } + + if ( $option ) { + $option = $option[1]; + } + + $extendList[] = $this->NewObj3( 'Less_Tree_Extend', array( $this->NewObj1( 'Less_Tree_Selector', $elements ), $option, $index ) ); + + }while ( $this->MatchChar( "," ) ); + + $this->expect( '/\\G\)/' ); + + if ( $isRule ) { + $this->expect( '/\\G;/' ); + } + + return $extendList; + } + + // + // A Mixin call, with an optional argument list + // + // #mixins > .square(#fff); + // .rounded(4px, black); + // .button; + // + // The `while` loop is there because mixins can be + // namespaced, but we only support the child and descendant + // selector for now. + // + private function parseMixinCall() { + $char = $this->input[$this->pos]; + if ( $char !== '.' && $char !== '#' ) { + return; + } + + $index = $this->pos; + $this->save(); // stop us absorbing part of an invalid selector + + $elements = $this->parseMixinCallElements(); + + if ( $elements ) { + + if ( $this->MatchChar( '(' ) ) { + $returned = $this->parseMixinArgs( true ); + $args = $returned['args']; + $this->expectChar( ')' ); + } else { + $args = array(); + } + + $important = $this->parseImportant(); + + if ( $this->parseEnd() ) { + $this->forget(); + return $this->NewObj5( 'Less_Tree_Mixin_Call', array( $elements, $args, $index, $this->env->currentFileInfo, $important ) ); + } + } + + $this->restore(); + } + + private function parseMixinCallElements() { + $elements = array(); + $c = null; + + while ( true ) { + $elemIndex = $this->pos; + $e = $this->MatchReg( '/\\G[#.](?:[\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/' ); + if ( !$e ) { + break; + } + $elements[] = $this->NewObj4( 'Less_Tree_Element', array( $c, $e[0], $elemIndex, $this->env->currentFileInfo ) ); + $c = $this->MatchChar( '>' ); + } + + return $elements; + } + + /** + * @param boolean $isCall + */ + private function parseMixinArgs( $isCall ) { + $expressions = array(); + $argsSemiColon = array(); + $isSemiColonSeperated = null; + $argsComma = array(); + $expressionContainsNamed = null; + $name = null; + $returner = array( 'args' => array(), 'variadic' => false ); + + $this->save(); + + while ( true ) { + if ( $isCall ) { + $arg = $this->MatchFuncs( array( 'parseDetachedRuleset','parseExpression' ) ); + } else { + $this->parseComments(); + if ( $this->input[ $this->pos ] === '.' && $this->MatchReg( '/\\G\.{3}/' ) ) { + $returner['variadic'] = true; + if ( $this->MatchChar( ";" ) && !$isSemiColonSeperated ) { + $isSemiColonSeperated = true; + } + + if ( $isSemiColonSeperated ) { + $argsSemiColon[] = array( 'variadic' => true ); + } else { + $argsComma[] = array( 'variadic' => true ); + } + break; + } + $arg = $this->MatchFuncs( array( 'parseEntitiesVariable','parseEntitiesLiteral','parseEntitiesKeyword' ) ); + } + + if ( !$arg ) { + break; + } + + $nameLoop = null; + if ( $arg instanceof Less_Tree_Expression ) { + $arg->throwAwayComments(); + } + $value = $arg; + $val = null; + + if ( $isCall ) { + // Variable + if ( property_exists( $arg, 'value' ) && count( $arg->value ) == 1 ) { + $val = $arg->value[0]; + } + } else { + $val = $arg; + } + + if ( $val instanceof Less_Tree_Variable ) { + + if ( $this->MatchChar( ':' ) ) { + if ( $expressions ) { + if ( $isSemiColonSeperated ) { + $this->Error( 'Cannot mix ; and , as delimiter types' ); + } + $expressionContainsNamed = true; + } + + // we do not support setting a ruleset as a default variable - it doesn't make sense + // However if we do want to add it, there is nothing blocking it, just don't error + // and remove isCall dependency below + $value = null; + if ( $isCall ) { + $value = $this->parseDetachedRuleset(); + } + if ( !$value ) { + $value = $this->parseExpression(); + } + + if ( !$value ) { + if ( $isCall ) { + $this->Error( 'could not understand value for named argument' ); + } else { + $this->restore(); + $returner['args'] = array(); + return $returner; + } + } + + $nameLoop = ( $name = $val->name ); + } elseif ( !$isCall && $this->MatchReg( '/\\G\.{3}/' ) ) { + $returner['variadic'] = true; + if ( $this->MatchChar( ";" ) && !$isSemiColonSeperated ) { + $isSemiColonSeperated = true; + } + if ( $isSemiColonSeperated ) { + $argsSemiColon[] = array( 'name' => $arg->name, 'variadic' => true ); + } else { + $argsComma[] = array( 'name' => $arg->name, 'variadic' => true ); + } + break; + } elseif ( !$isCall ) { + $name = $nameLoop = $val->name; + $value = null; + } + } + + if ( $value ) { + $expressions[] = $value; + } + + $argsComma[] = array( 'name' => $nameLoop, 'value' => $value ); + + if ( $this->MatchChar( ',' ) ) { + continue; + } + + if ( $this->MatchChar( ';' ) || $isSemiColonSeperated ) { + + if ( $expressionContainsNamed ) { + $this->Error( 'Cannot mix ; and , as delimiter types' ); + } + + $isSemiColonSeperated = true; + + if ( count( $expressions ) > 1 ) { + $value = $this->NewObj1( 'Less_Tree_Value', $expressions ); + } + $argsSemiColon[] = array( 'name' => $name, 'value' => $value ); + + $name = null; + $expressions = array(); + $expressionContainsNamed = false; + } + } + + $this->forget(); + $returner['args'] = ( $isSemiColonSeperated ? $argsSemiColon : $argsComma ); + return $returner; + } + + // + // A Mixin definition, with a list of parameters + // + // .rounded (@radius: 2px, @color) { + // ... + // } + // + // Until we have a finer grained state-machine, we have to + // do a look-ahead, to make sure we don't have a mixin call. + // See the `rule` function for more information. + // + // We start by matching `.rounded (`, and then proceed on to + // the argument list, which has optional default values. + // We store the parameters in `params`, with a `value` key, + // if there is a value, such as in the case of `@radius`. + // + // Once we've got our params list, and a closing `)`, we parse + // the `{...}` block. + // + private function parseMixinDefinition() { + $cond = null; + + $char = $this->input[$this->pos]; + if ( ( $char !== '.' && $char !== '#' ) || ( $char === '{' && $this->PeekReg( '/\\G[^{]*\}/' ) ) ) { + return; + } + + $this->save(); + + $match = $this->MatchReg( '/\\G([#.](?:[\w-]|\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/' ); + if ( $match ) { + $name = $match[1]; + + $argInfo = $this->parseMixinArgs( false ); + $params = $argInfo['args']; + $variadic = $argInfo['variadic']; + + // .mixincall("@{a}"); + // looks a bit like a mixin definition.. + // also + // .mixincall(@a: {rule: set;}); + // so we have to be nice and restore + if ( !$this->MatchChar( ')' ) ) { + $this->furthest = $this->pos; + $this->restore(); + return; + } + + $this->parseComments(); + + if ( $this->MatchReg( '/\\Gwhen/' ) ) { // Guard + $cond = $this->expect( 'parseConditions', 'Expected conditions' ); + } + + $ruleset = $this->parseBlock(); + + if ( is_array( $ruleset ) ) { + $this->forget(); + return $this->NewObj5( 'Less_Tree_Mixin_Definition', array( $name, $params, $ruleset, $cond, $variadic ) ); + } + + $this->restore(); + } else { + $this->forget(); + } + } + + // + // Entities are the smallest recognized token, + // and can be found inside a rule's value. + // + private function parseEntity() { + return $this->MatchFuncs( array( 'parseEntitiesLiteral','parseEntitiesVariable','parseEntitiesUrl','parseEntitiesCall','parseEntitiesKeyword','parseEntitiesJavascript','parseComment' ) ); + } + + // + // A Rule terminator. Note that we use `peek()` to check for '}', + // because the `block` rule will be expecting it, but we still need to make sure + // it's there, if ';' was omitted. + // + private function parseEnd() { + return $this->MatchChar( ';' ) || $this->PeekChar( '}' ); + } + + // + // IE's alpha function + // + // alpha(opacity=88) + // + private function parseAlpha() { + if ( !$this->MatchReg( '/\\G\(opacity=/i' ) ) { + return; + } + + $value = $this->MatchReg( '/\\G[0-9]+/' ); + if ( $value ) { + $value = $value[0]; + } else { + $value = $this->parseEntitiesVariable(); + if ( !$value ) { + return; + } + } + + $this->expectChar( ')' ); + return $this->NewObj1( 'Less_Tree_Alpha', $value ); + } + + // + // A Selector Element + // + // div + // + h1 + // #socks + // input[type="text"] + // + // Elements are the building blocks for Selectors, + // they are made out of a `Combinator` (see combinator rule), + // and an element name, such as a tag a class, or `*`. + // + private function parseElement() { + $c = $this->parseCombinator(); + $index = $this->pos; + + $e = $this->match( array( '/\\G(?:\d+\.\d+|\d+)%/', '/\\G(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/', + '#*', '#&', 'parseAttribute', '/\\G\([^()@]+\)/', '/\\G[\.#](?=@)/', 'parseEntitiesVariableCurly' ) ); + + if ( is_null( $e ) ) { + $this->save(); + if ( $this->MatchChar( '(' ) ) { + if ( ( $v = $this->parseSelector() ) && $this->MatchChar( ')' ) ) { + $e = $this->NewObj1( 'Less_Tree_Paren', $v ); + $this->forget(); + } else { + $this->restore(); + } + } else { + $this->forget(); + } + } + + if ( !is_null( $e ) ) { + return $this->NewObj4( 'Less_Tree_Element', array( $c, $e, $index, $this->env->currentFileInfo ) ); + } + } + + // + // Combinators combine elements together, in a Selector. + // + // Because our parser isn't white-space sensitive, special care + // has to be taken, when parsing the descendant combinator, ` `, + // as it's an empty space. We have to check the previous character + // in the input, to see if it's a ` ` character. + // + private function parseCombinator() { + if ( $this->pos < $this->input_len ) { + $c = $this->input[$this->pos]; + if ( $c === '>' || $c === '+' || $c === '~' || $c === '|' || $c === '^' ) { + + $this->pos++; + if ( $this->input[$this->pos] === '^' ) { + $c = '^^'; + $this->pos++; + } + + $this->skipWhitespace( 0 ); + + return $c; + } + + if ( $this->pos > 0 && $this->isWhitespace( -1 ) ) { + return ' '; + } + } + } + + // + // A CSS selector (see selector below) + // with less extensions e.g. the ability to extend and guard + // + private function parseLessSelector() { + return $this->parseSelector( true ); + } + + // + // A CSS Selector + // + // .class > div + h1 + // li a:hover + // + // Selectors are made out of one or more Elements, see above. + // + private function parseSelector( $isLess = false ) { + $elements = array(); + $extendList = array(); + $condition = null; + $when = false; + $extend = false; + $e = null; + $c = null; + $index = $this->pos; + + while ( ( $isLess && ( $extend = $this->parseExtend() ) ) || ( $isLess && ( $when = $this->MatchReg( '/\\Gwhen/' ) ) ) || ( $e = $this->parseElement() ) ) { + if ( $when ) { + $condition = $this->expect( 'parseConditions', 'expected condition' ); + } elseif ( $condition ) { + // error("CSS guard can only be used at the end of selector"); + } elseif ( $extend ) { + $extendList = array_merge( $extendList, $extend ); + } else { + // if( count($extendList) ){ + //error("Extend can only be used at the end of selector"); + //} + if ( $this->pos < $this->input_len ) { + $c = $this->input[ $this->pos ]; + } + $elements[] = $e; + $e = null; + } + + if ( $c === '{' || $c === '}' || $c === ';' || $c === ',' || $c === ')' ) { break; + } + } + + if ( $elements ) { + return $this->NewObj5( 'Less_Tree_Selector', array( $elements, $extendList, $condition, $index, $this->env->currentFileInfo ) ); + } + if ( $extendList ) { + $this->Error( 'Extend must be used to extend a selector, it cannot be used on its own' ); + } + } + + private function parseTag() { + return ( $tag = $this->MatchReg( '/\\G[A-Za-z][A-Za-z-]*[0-9]?/' ) ) ? $tag : $this->MatchChar( '*' ); + } + + private function parseAttribute() { + $val = null; + + if ( !$this->MatchChar( '[' ) ) { + return; + } + + $key = $this->parseEntitiesVariableCurly(); + if ( !$key ) { + $key = $this->expect( '/\\G(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\\\.)+/' ); + } + + $op = $this->MatchReg( '/\\G[|~*$^]?=/' ); + if ( $op ) { + $val = $this->match( array( 'parseEntitiesQuoted','/\\G[0-9]+%/','/\\G[\w-]+/','parseEntitiesVariableCurly' ) ); + } + + $this->expectChar( ']' ); + + return $this->NewObj3( 'Less_Tree_Attribute', array( $key, $op === null ? null : $op[0], $val ) ); + } + + // + // The `block` rule is used by `ruleset` and `mixin.definition`. + // It's a wrapper around the `primary` rule, with added `{}`. + // + private function parseBlock() { + if ( $this->MatchChar( '{' ) ) { + $content = $this->parsePrimary(); + if ( $this->MatchChar( '}' ) ) { + return $content; + } + } + } + + private function parseBlockRuleset() { + $block = $this->parseBlock(); + + if ( $block ) { + $block = $this->NewObj2( 'Less_Tree_Ruleset', array( null, $block ) ); + } + + return $block; + } + + private function parseDetachedRuleset() { + $blockRuleset = $this->parseBlockRuleset(); + if ( $blockRuleset ) { + return $this->NewObj1( 'Less_Tree_DetachedRuleset', $blockRuleset ); + } + } + + // + // div, .class, body > p {...} + // + private function parseRuleset() { + $selectors = array(); + + $this->save(); + + while ( true ) { + $s = $this->parseLessSelector(); + if ( !$s ) { + break; + } + $selectors[] = $s; + $this->parseComments(); + + if ( $s->condition && count( $selectors ) > 1 ) { + $this->Error( 'Guards are only currently allowed on a single selector.' ); + } + + if ( !$this->MatchChar( ',' ) ) { + break; + } + if ( $s->condition ) { + $this->Error( 'Guards are only currently allowed on a single selector.' ); + } + $this->parseComments(); + } + + if ( $selectors ) { + $rules = $this->parseBlock(); + if ( is_array( $rules ) ) { + $this->forget(); + return $this->NewObj2( 'Less_Tree_Ruleset', array( $selectors, $rules ) ); // Less_Environment::$strictImports + } + } + + // Backtrack + $this->furthest = $this->pos; + $this->restore(); + } + + /** + * Custom less.php parse function for finding simple name-value css pairs + * ex: width:100px; + */ + private function parseNameValue() { + $index = $this->pos; + $this->save(); + + // $match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*((?:\'")?[a-zA-Z0-9\-% \.,!]+?(?:\'")?)\s*([;}])/'); + $match = $this->MatchReg( '/\\G([a-zA-Z\-]+)\s*:\s*([\'"]?[#a-zA-Z0-9\-%\.,]+?[\'"]?) *(! *important)?\s*([;}])/' ); + if ( $match ) { + + if ( $match[4] == '}' ) { + $this->pos = $index + strlen( $match[0] ) - 1; + } + + if ( $match[3] ) { + $match[2] .= ' !important'; + } + + return $this->NewObj4( 'Less_Tree_NameValue', array( $match[1], $match[2], $index, $this->env->currentFileInfo ) ); + } + + $this->restore(); + } + + private function parseRule( $tryAnonymous = null ) { + $merge = false; + $startOfRule = $this->pos; + + $c = $this->input[$this->pos]; + if ( $c === '.' || $c === '#' || $c === '&' ) { + return; + } + + $this->save(); + $name = $this->MatchFuncs( array( 'parseVariable','parseRuleProperty' ) ); + + if ( $name ) { + + $isVariable = is_string( $name ); + + $value = null; + if ( $isVariable ) { + $value = $this->parseDetachedRuleset(); + } + + $important = null; + if ( !$value ) { + + // prefer to try to parse first if its a variable or we are compressing + // but always fallback on the other one + //if( !$tryAnonymous && is_string($name) && $name[0] === '@' ){ + if ( !$tryAnonymous && ( Less_Parser::$options['compress'] || $isVariable ) ) { + $value = $this->MatchFuncs( array( 'parseValue','parseAnonymousValue' ) ); + } else { + $value = $this->MatchFuncs( array( 'parseAnonymousValue','parseValue' ) ); + } + + $important = $this->parseImportant(); + + // a name returned by this.ruleProperty() is always an array of the form: + // [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"] + // where each item is a tree.Keyword or tree.Variable + if ( !$isVariable && is_array( $name ) ) { + $nm = array_pop( $name ); + if ( $nm->value ) { + $merge = $nm->value; + } + } + } + + if ( $value && $this->parseEnd() ) { + $this->forget(); + return $this->NewObj6( 'Less_Tree_Rule', array( $name, $value, $important, $merge, $startOfRule, $this->env->currentFileInfo ) ); + } else { + $this->furthest = $this->pos; + $this->restore(); + if ( $value && !$tryAnonymous ) { + return $this->parseRule( true ); + } + } + } else { + $this->forget(); + } + } + + function parseAnonymousValue() { + if ( preg_match( '/\\G([^@+\/\'"*`(;{}-]*);/', $this->input, $match, 0, $this->pos ) ) { + $this->pos += strlen( $match[1] ); + return $this->NewObj1( 'Less_Tree_Anonymous', $match[1] ); + } + } + + // + // An @import directive + // + // @import "lib"; + // + // Depending on our environment, importing is done differently: + // In the browser, it's an XHR request, in Node, it would be a + // file-system operation. The function used for importing is + // stored in `import`, which we pass to the Import constructor. + // + private function parseImport() { + $this->save(); + + $dir = $this->MatchReg( '/\\G@import?\s+/' ); + + if ( $dir ) { + $options = $this->parseImportOptions(); + $path = $this->MatchFuncs( array( 'parseEntitiesQuoted','parseEntitiesUrl' ) ); + + if ( $path ) { + $features = $this->parseMediaFeatures(); + if ( $this->MatchChar( ';' ) ) { + if ( $features ) { + $features = $this->NewObj1( 'Less_Tree_Value', $features ); + } + + $this->forget(); + return $this->NewObj5( 'Less_Tree_Import', array( $path, $features, $options, $this->pos, $this->env->currentFileInfo ) ); + } + } + } + + $this->restore(); + } + + private function parseImportOptions() { + $options = array(); + + // list of options, surrounded by parens + if ( !$this->MatchChar( '(' ) ) { + return $options; + } + do{ + $optionName = $this->parseImportOption(); + if ( $optionName ) { + $value = true; + switch ( $optionName ) { + case "css": + $optionName = "less"; + $value = false; + break; + case "once": + $optionName = "multiple"; + $value = false; + break; + } + $options[$optionName] = $value; + if ( !$this->MatchChar( ',' ) ) { break; + } + } + }while ( $optionName ); + $this->expectChar( ')' ); + return $options; + } + + private function parseImportOption() { + $opt = $this->MatchReg( '/\\G(less|css|multiple|once|inline|reference|optional)/' ); + if ( $opt ) { + return $opt[1]; + } + } + + private function parseMediaFeature() { + $nodes = array(); + + do{ + $e = $this->MatchFuncs( array( 'parseEntitiesKeyword','parseEntitiesVariable' ) ); + if ( $e ) { + $nodes[] = $e; + } elseif ( $this->MatchChar( '(' ) ) { + $p = $this->parseProperty(); + $e = $this->parseValue(); + if ( $this->MatchChar( ')' ) ) { + if ( $p && $e ) { + $r = $this->NewObj7( 'Less_Tree_Rule', array( $p, $e, null, null, $this->pos, $this->env->currentFileInfo, true ) ); + $nodes[] = $this->NewObj1( 'Less_Tree_Paren', $r ); + } elseif ( $e ) { + $nodes[] = $this->NewObj1( 'Less_Tree_Paren', $e ); + } else { + return null; + } + } else return null; + } + } while ( $e ); + + if ( $nodes ) { + return $this->NewObj1( 'Less_Tree_Expression', $nodes ); + } + } + + private function parseMediaFeatures() { + $features = array(); + + do { + $e = $this->parseMediaFeature(); + if ( $e ) { + $features[] = $e; + if ( !$this->MatchChar( ',' ) ) break; + } else { + $e = $this->parseEntitiesVariable(); + if ( $e ) { + $features[] = $e; + if ( !$this->MatchChar( ',' ) ) break; + } + } + } while ( $e ); + + return $features ?: null; + } + + private function parseMedia() { + if ( $this->MatchReg( '/\\G@media/' ) ) { + $features = $this->parseMediaFeatures(); + $rules = $this->parseBlock(); + + if ( is_array( $rules ) ) { + return $this->NewObj4( 'Less_Tree_Media', array( $rules, $features, $this->pos, $this->env->currentFileInfo ) ); + } + } + } + + // + // A CSS Directive + // + // @charset "utf-8"; + // + private function parseDirective() { + if ( !$this->PeekChar( '@' ) ) { + return; + } + + $rules = null; + $index = $this->pos; + $hasBlock = true; + $hasIdentifier = false; + $hasExpression = false; + $hasUnknown = false; + + $value = $this->MatchFuncs( array( 'parseImport','parseMedia' ) ); + if ( $value ) { + return $value; + } + + $this->save(); + + $name = $this->MatchReg( '/\\G@[a-z-]+/' ); + + if ( !$name ) return; + $name = $name[0]; + + $nonVendorSpecificName = $name; + $pos = strpos( $name, '-', 2 ); + if ( $name[1] == '-' && $pos > 0 ) { + $nonVendorSpecificName = "@" . substr( $name, $pos + 1 ); + } + + switch ( $nonVendorSpecificName ) { + /* + case "@font-face": + case "@viewport": + case "@top-left": + case "@top-left-corner": + case "@top-center": + case "@top-right": + case "@top-right-corner": + case "@bottom-left": + case "@bottom-left-corner": + case "@bottom-center": + case "@bottom-right": + case "@bottom-right-corner": + case "@left-top": + case "@left-middle": + case "@left-bottom": + case "@right-top": + case "@right-middle": + case "@right-bottom": + hasBlock = true; + break; + */ + case "@charset": + $hasIdentifier = true; + $hasBlock = false; + break; + case "@namespace": + $hasExpression = true; + $hasBlock = false; + break; + case "@keyframes": + $hasIdentifier = true; + break; + case "@host": + case "@page": + case "@document": + case "@supports": + $hasUnknown = true; + break; + } + + if ( $hasIdentifier ) { + $value = $this->parseEntity(); + if ( !$value ) { + $this->error( "expected " . $name . " identifier" ); + } + } else if ( $hasExpression ) { + $value = $this->parseExpression(); + if ( !$value ) { + $this->error( "expected " . $name. " expression" ); + } + } else if ( $hasUnknown ) { + + $value = $this->MatchReg( '/\\G[^{;]+/' ); + if ( $value ) { + $value = $this->NewObj1( 'Less_Tree_Anonymous', trim( $value[0] ) ); + } + } + + if ( $hasBlock ) { + $rules = $this->parseBlockRuleset(); + } + + if ( $rules || ( !$hasBlock && $value && $this->MatchChar( ';' ) ) ) { + $this->forget(); + return $this->NewObj5( 'Less_Tree_Directive', array( $name, $value, $rules, $index, $this->env->currentFileInfo ) ); + } + + $this->restore(); + } + + // + // A Value is a comma-delimited list of Expressions + // + // font-family: Baskerville, Georgia, serif; + // + // In a Rule, a Value represents everything after the `:`, + // and before the `;`. + // + private function parseValue() { + $expressions = array(); + + do{ + $e = $this->parseExpression(); + if ( $e ) { + $expressions[] = $e; + if ( !$this->MatchChar( ',' ) ) { + break; + } + } + }while ( $e ); + + if ( $expressions ) { + return $this->NewObj1( 'Less_Tree_Value', $expressions ); + } + } + + private function parseImportant() { + if ( $this->PeekChar( '!' ) && $this->MatchReg( '/\\G! *important/' ) ) { + return ' !important'; + } + } + + private function parseSub() { + if ( $this->MatchChar( '(' ) ) { + $a = $this->parseAddition(); + if ( $a ) { + $this->expectChar( ')' ); + return $this->NewObj2( 'Less_Tree_Expression', array( array( $a ), true ) ); // instead of $e->parens = true so the value is cached + } + } + } + + /** + * Parses multiplication operation + * + * @return Less_Tree_Operation|null + */ + function parseMultiplication() { + $return = $m = $this->parseOperand(); + if ( $return ) { + while ( true ) { + + $isSpaced = $this->isWhitespace( -1 ); + + if ( $this->PeekReg( '/\\G\/[*\/]/' ) ) { + break; + } + + $op = $this->MatchChar( '/' ); + if ( !$op ) { + $op = $this->MatchChar( '*' ); + if ( !$op ) { + break; + } + } + + $a = $this->parseOperand(); + + if ( !$a ) { break; + } + + $m->parensInOp = true; + $a->parensInOp = true; + $return = $this->NewObj3( 'Less_Tree_Operation', array( $op, array( $return, $a ), $isSpaced ) ); + } + } + return $return; + + } + + /** + * Parses an addition operation + * + * @return Less_Tree_Operation|null + */ + private function parseAddition() { + $return = $m = $this->parseMultiplication(); + if ( $return ) { + while ( true ) { + + $isSpaced = $this->isWhitespace( -1 ); + + $op = $this->MatchReg( '/\\G[-+]\s+/' ); + if ( $op ) { + $op = $op[0]; + } else { + if ( !$isSpaced ) { + $op = $this->match( array( '#+','#-' ) ); + } + if ( !$op ) { + break; + } + } + + $a = $this->parseMultiplication(); + if ( !$a ) { + break; + } + + $m->parensInOp = true; + $a->parensInOp = true; + $return = $this->NewObj3( 'Less_Tree_Operation', array( $op, array( $return, $a ), $isSpaced ) ); + } + } + + return $return; + } + + /** + * Parses the conditions + * + * @return Less_Tree_Condition|null + */ + private function parseConditions() { + $index = $this->pos; + $return = $a = $this->parseCondition(); + if ( $a ) { + while ( true ) { + if ( !$this->PeekReg( '/\\G,\s*(not\s*)?\(/' ) || !$this->MatchChar( ',' ) ) { + break; + } + $b = $this->parseCondition(); + if ( !$b ) { + break; + } + + $return = $this->NewObj4( 'Less_Tree_Condition', array( 'or', $return, $b, $index ) ); + } + return $return; + } + } + + private function parseCondition() { + $index = $this->pos; + $negate = false; + $c = null; + + if ( $this->MatchReg( '/\\Gnot/' ) ) $negate = true; + $this->expectChar( '(' ); + $a = $this->MatchFuncs( array( 'parseAddition','parseEntitiesKeyword','parseEntitiesQuoted' ) ); + + if ( $a ) { + $op = $this->MatchReg( '/\\G(?:>=|<=|=<|[<=>])/' ); + if ( $op ) { + $b = $this->MatchFuncs( array( 'parseAddition','parseEntitiesKeyword','parseEntitiesQuoted' ) ); + if ( $b ) { + $c = $this->NewObj5( 'Less_Tree_Condition', array( $op[0], $a, $b, $index, $negate ) ); + } else { + $this->Error( 'Unexpected expression' ); + } + } else { + $k = $this->NewObj1( 'Less_Tree_Keyword', 'true' ); + $c = $this->NewObj5( 'Less_Tree_Condition', array( '=', $a, $k, $index, $negate ) ); + } + $this->expectChar( ')' ); + return $this->MatchReg( '/\\Gand/' ) ? $this->NewObj3( 'Less_Tree_Condition', array( 'and', $c, $this->parseCondition() ) ) : $c; + } + } + + /** + * An operand is anything that can be part of an operation, + * such as a Color, or a Variable + */ + private function parseOperand() { + $negate = false; + $offset = $this->pos + 1; + if ( $offset >= $this->input_len ) { + return; + } + $char = $this->input[$offset]; + if ( $char === '@' || $char === '(' ) { + $negate = $this->MatchChar( '-' ); + } + + $o = $this->MatchFuncs( array( 'parseSub','parseEntitiesDimension','parseEntitiesColor','parseEntitiesVariable','parseEntitiesCall' ) ); + + if ( $negate ) { + $o->parensInOp = true; + $o = $this->NewObj1( 'Less_Tree_Negative', $o ); + } + + return $o; + } + + /** + * Expressions either represent mathematical operations, + * or white-space delimited Entities. + * + * @return Less_Tree_Expression|null + */ + private function parseExpression() { + $entities = array(); + + do { + $e = $this->MatchFuncs( array( 'parseAddition','parseEntity' ) ); + if ( $e ) { + $entities[] = $e; + // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here + if ( !$this->PeekReg( '/\\G\/[\/*]/' ) ) { + $delim = $this->MatchChar( '/' ); + if ( $delim ) { + $entities[] = $this->NewObj1( 'Less_Tree_Anonymous', $delim ); + } + } + } + } while ( $e ); + + if ( $entities ) { + return $this->NewObj1( 'Less_Tree_Expression', $entities ); + } + } + + /** + * Parse a property + * eg: 'min-width', 'orientation', etc + * + * @return string + */ + private function parseProperty() { + $name = $this->MatchReg( '/\\G(\*?-?[_a-zA-Z0-9-]+)\s*:/' ); + if ( $name ) { + return $name[1]; + } + } + + /** + * Parse a rule property + * eg: 'color', 'width', 'height', etc + * + * @return string + */ + private function parseRuleProperty() { + $offset = $this->pos; + $name = array(); + $index = array(); + $length = 0; + + $this->rulePropertyMatch( '/\\G(\*?)/', $offset, $length, $index, $name ); + while ( $this->rulePropertyMatch( '/\\G((?:[\w-]+)|(?:@\{[\w-]+\}))/', $offset, $length, $index, $name ) ); // ! + + if ( ( count( $name ) > 1 ) && $this->rulePropertyMatch( '/\\G\s*((?:\+_|\+)?)\s*:/', $offset, $length, $index, $name ) ) { + // at last, we have the complete match now. move forward, + // convert name particles to tree objects and return: + $this->skipWhitespace( $length ); + + if ( $name[0] === '' ) { + array_shift( $name ); + array_shift( $index ); + } + foreach ( $name as $k => $s ) { + if ( !$s || $s[0] !== '@' ) { + $name[$k] = $this->NewObj1( 'Less_Tree_Keyword', $s ); + } else { + $name[$k] = $this->NewObj3( 'Less_Tree_Variable', array( '@' . substr( $s, 2, -1 ), $index[$k], $this->env->currentFileInfo ) ); + } + } + return $name; + } + + } + + private function rulePropertyMatch( $re, &$offset, &$length, &$index, &$name ) { + preg_match( $re, $this->input, $a, 0, $offset ); + if ( $a ) { + $index[] = $this->pos + $length; + $length += strlen( $a[0] ); + $offset += strlen( $a[0] ); + $name[] = $a[1]; + return true; + } + } + + public static function serializeVars( $vars ) { + $s = ''; + + foreach ( $vars as $name => $value ) { + $s .= ( ( $name[0] === '@' ) ? '' : '@' ) . $name .': '. $value . ( ( substr( $value, -1 ) === ';' ) ? '' : ';' ); + } + + return $s; + } + + /** + * Some versions of php have trouble with method_exists($a,$b) if $a is not an object + * + * @param string $b + */ + public static function is_method( $a, $b ) { + return is_object( $a ) && method_exists( $a, $b ); + } + + /** + * Round numbers similarly to javascript + * eg: 1.499999 to 1 instead of 2 + */ + public static function round( $input, $precision = 0 ) { + $precision = pow( 10, $precision ); + $i = $input * $precision; + + $ceil = ceil( $i ); + $floor = floor( $i ); + if ( ( $ceil - $i ) <= ( $i - $floor ) ) { + return $ceil / $precision; + } else { + return $floor / $precision; + } + } + + /** + * Create Less_Tree_* objects and optionally generate a cache string + * + * @return mixed + */ + public function NewObj0( $class ) { + $obj = new $class(); + if ( $this->CacheEnabled() ) { + $obj->cache_string = ' new '.$class.'()'; + } + return $obj; + } + + public function NewObj1( $class, $arg ) { + $obj = new $class( $arg ); + if ( $this->CacheEnabled() ) { + $obj->cache_string = ' new '.$class.'('.Less_Parser::ArgString( $arg ).')'; + } + return $obj; + } + + public function NewObj2( $class, $args ) { + $obj = new $class( $args[0], $args[1] ); + if ( $this->CacheEnabled() ) { + $this->ObjCache( $obj, $class, $args ); + } + return $obj; + } + + public function NewObj3( $class, $args ) { + $obj = new $class( $args[0], $args[1], $args[2] ); + if ( $this->CacheEnabled() ) { + $this->ObjCache( $obj, $class, $args ); + } + return $obj; + } + + public function NewObj4( $class, $args ) { + $obj = new $class( $args[0], $args[1], $args[2], $args[3] ); + if ( $this->CacheEnabled() ) { + $this->ObjCache( $obj, $class, $args ); + } + return $obj; + } + + public function NewObj5( $class, $args ) { + $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4] ); + if ( $this->CacheEnabled() ) { + $this->ObjCache( $obj, $class, $args ); + } + return $obj; + } + + public function NewObj6( $class, $args ) { + $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5] ); + if ( $this->CacheEnabled() ) { + $this->ObjCache( $obj, $class, $args ); + } + return $obj; + } + + public function NewObj7( $class, $args ) { + $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6] ); + if ( $this->CacheEnabled() ) { + $this->ObjCache( $obj, $class, $args ); + } + return $obj; + } + + // caching + public function ObjCache( $obj, $class, $args = array() ) { + $obj->cache_string = ' new '.$class.'('. self::ArgCache( $args ).')'; + } + + public function ArgCache( $args ) { + return implode( ',', array_map( array( 'Less_Parser','ArgString' ), $args ) ); + } + + /** + * Convert an argument to a string for use in the parser cache + * + * @return string + */ + public static function ArgString( $arg ) { + $type = gettype( $arg ); + + if ( $type === 'object' ) { + $string = $arg->cache_string; + unset( $arg->cache_string ); + return $string; + + } elseif ( $type === 'array' ) { + $string = ' Array('; + foreach ( $arg as $k => $a ) { + $string .= var_export( $k, true ).' => '.self::ArgString( $a ).','; + } + return $string . ')'; + } + + return var_export( $arg, true ); + } + + public function Error( $msg ) { + throw new Less_Exception_Parser( $msg, null, $this->furthest, $this->env->currentFileInfo ); + } + + public static function WinPath( $path ) { + return str_replace( '\\', '/', $path ); + } + + public static function AbsPath( $path, $winPath = false ) { + if ( strpos( $path, '//' ) !== false && preg_match( '_^(https?:)?//\\w+(\\.\\w+)+/\\w+_i', $path ) ) { + return $winPath ? '' : false; + } else { + $path = realpath( $path ); + if ( $winPath ) { + $path = self::WinPath( $path ); + } + return $path; + } + } + + public function CacheEnabled() { + return ( Less_Parser::$options['cache_method'] && ( Less_Cache::$cache_dir || ( Less_Parser::$options['cache_method'] == 'callback' ) ) ); + } + +} |