You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2627 lines
60 KiB
2627 lines
60 KiB
<?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($options);
|
|
$this->env->Init();
|
|
|
|
//set new options
|
|
if( is_array($options) ){
|
|
$this->SetOptions(Less_Parser::$default_options);
|
|
$this->SetOptions($options);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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::WinPath(realpath($filename));
|
|
}
|
|
$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(){
|
|
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);
|
|
}
|
|
|
|
|
|
private function isWhitespace($offset = 0) {
|
|
return preg_match('/\s/',$this->input[ $this->pos + $offset]);
|
|
}
|
|
|
|
/**
|
|
* 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 ){
|
|
$this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $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
|
|
}
|
|
|
|
if( $this->input[$j] != '"' && $this->input[$j] !== "'" ){
|
|
return;
|
|
}
|
|
|
|
if ($e) {
|
|
$this->MatchChar('~');
|
|
}
|
|
|
|
// Fix for #124: match escaped newlines
|
|
//$str = $this->MatchReg('/\\G"((?:[^"\\\\\r\n]|\\\\.)*)"|\'((?:[^\'\\\\\r\n]|\\\\.)*)\'/');
|
|
$str = $this->MatchReg('/\\G"((?:[^"\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)"|\'((?:[^\'\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)\'/');
|
|
|
|
if( $str ){
|
|
$result = $str[0][0] == '"' ? $str[1] : $str[2];
|
|
return $this->NewObj5('Less_Tree_Quoted',array($str[0], $result, $e, $index, $this->env->currentFileInfo) );
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
//
|
|
// 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 useing 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 ommitted.
|
|
//
|
|
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[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 function CacheEnabled(){
|
|
return (Less_Parser::$options['cache_method'] && (Less_Cache::$cache_dir || (Less_Parser::$options['cache_method'] == 'callback')));
|
|
}
|
|
|
|
}
|
|
|