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( $options ); // 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 bool|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 '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 ); } return false; } 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 $uri_root 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, '' ); break; case 'var_export': // Requires __set_state() file_put_contents( $cache_file, '' ); 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() { unset( $this->input, $this->pos, $this->input_len, $this->furthest ); $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 ? $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. * * 1px solid black * @var * 2 * * @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( $i, $precision = 0 ) { $precision = pow( 10, $precision ); $i = $i * $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' ) ) ); } }