Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F1429756
Parser.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
82 KB
Referenced Files
None
Subscribers
None
Parser.php
View Options
<?php
/**
* Parse and compile Less files into CSS
*/
class
Less_Parser
{
/**
* Default parser options
* @var array<string,mixed>
*/
public
static
$default_options
=
[
'compress'
=>
false
,
// option - whether to compress
'strictUnits'
=>
false
,
// whether units need to evaluate correctly
/* How to process math
* always - eagerly try to solve all operations
* parens-division - require parens for division "/"
* parens | strict - require parens for all operations
*/
// NOTE: We use the default of Less.js 4.0 (parens-division)
// instead of Less.js 3.13 (always).
'math'
=>
'parens-division'
,
'relativeUrls'
=>
true
,
// option - whether to adjust URL's to be relative
'urlArgs'
=>
''
,
// whether to add args into url tokens
'numPrecision'
=>
8
,
'import_dirs'
=>
[],
'cache_dir'
=>
null
,
'cache_method'
=>
'serialize'
,
// false, 'serialize', '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'
=>
[],
'functions'
=>
[],
];
/** @var array{compress:bool,strictUnits:bool,relativeUrls:bool,urlArgs:string,numPrecision:int,import_dirs:array,indentation:string} */
public
static
$options
=
[];
private
$input
;
// Less input string
private
$input_len
;
// input string length
private
$pos
;
// current index in `input`
private
$saveStack
=
[];
// holds state for backtracking
private
$furthest
;
private
$mb_internal_encoding
=
''
;
// for remember exists value of mbstring.internal_encoding
private
$autoCommentAbsorb
=
true
;
/**
* @var array<array{index:int,text:string,isLineComment:bool}>
*/
private
$commentStore
=
[];
/**
* @var Less_Environment
*/
private
$env
;
protected
$rules
=
[];
/**
* Evaluated ruleset created by `getCss()`. Stored for potential use in `getVariables()`
* @var Less_Tree[]|null
*/
private
$cachedEvaldRules
;
public
static
$has_extends
=
false
;
public
static
$next_id
=
0
;
/**
* Filename to contents of all parsed the files
*
* @var array
*/
public
static
$contentsMap
=
[];
/**
* @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
->
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'
);
}
Less_Tree
::
$parse
=
$this
;
}
/**
* Reset the parser state completely
*/
public
function
Reset
(
$options
=
null
)
{
$this
->
rules
=
[];
$this
->
cachedEvaldRules
=
null
;
self
::
$has_extends
=
false
;
self
::
$contentsMap
=
[];
$this
->
env
=
new
Less_Environment
();
// set new options
$this
->
SetOptions
(
self
::
$default_options
);
if
(
is_array
(
$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
'strictMath'
:
if
(
$value
)
{
$this
->
env
->
math
=
Less_Environment
::
MATH_PARENS
;
}
else
{
$this
->
env
->
math
=
Less_Environment
::
MATH_ALWAYS
;
}
break
;
case
'math'
:
$value
=
strtolower
(
$value
);
if
(
$value
===
'always'
)
{
$this
->
env
->
math
=
Less_Environment
::
MATH_ALWAYS
;
}
elseif
(
$value
===
'parens-division'
)
{
$this
->
env
->
math
=
Less_Environment
::
MATH_PARENS_DIVISION
;
}
elseif
(
$value
===
'parens'
||
$value
===
'strict'
)
{
$this
->
env
->
math
=
Less_Environment
::
MATH_PARENS
;
}
return
;
case
'import_dirs'
:
$this
->
SetImportDirs
(
$value
);
return
;
case
'cache_dir'
:
if
(
is_string
(
$value
)
)
{
Less_Cache
::
SetCacheDir
(
$value
);
Less_Cache
::
CheckCacheDir
();
}
return
;
case
'functions'
:
foreach
(
$value
as
$key
=>
$function
)
{
$this
->
registerFunction
(
$key
,
$function
);
}
return
;
}
self
::
$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
(
null
,
$this
->
rules
);
$root
->
root
=
true
;
$root
->
firstRoot
=
true
;
$importVisitor
=
new
Less_ImportVisitor
(
$this
->
env
);
$importVisitor
->
run
(
$root
);
$this
->
PreVisitors
(
$root
);
self
::
$has_extends
=
false
;
$evaldRoot
=
$root
->
compile
(
$this
->
env
);
$this
->
cachedEvaldRules
=
$evaldRoot
->
rules
;
$this
->
PostVisitors
(
$evaldRoot
);
if
(
self
::
$options
[
'sourceMap'
]
)
{
$generator
=
new
Less_SourceMap_Generator
(
$evaldRoot
,
self
::
$contentsMap
,
self
::
$options
);
// will also save file
// FIXME: should happen somewhere else?
$css
=
$generator
->
generateCSS
();
}
else
{
$css
=
$evaldRoot
->
toCSS
();
}
if
(
self
::
$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
)
{
$rules
=
$this
->
cachedEvaldRules
??
$this
->
rules
;
foreach
(
$rules
as
$rule
)
{
if
(
isset
(
$rule
->
variable
)
&&
(
$rule
->
variable
==
true
)
&&
(
str_replace
(
"@"
,
""
,
$rule
->
name
)
==
$varName
)
)
{
return
$this
->
getVariableValue
(
$rule
);
}
}
return
null
;
}
/**
* Get an array of the found variables in the parsed input.
*
* @return array
* @phan-return array<string,string|float|array>
*/
public
function
getVariables
()
{
$variables
=
[];
$rules
=
$this
->
cachedEvaldRules
??
$this
->
rules
;
foreach
(
$rules
as
$key
=>
$rule
)
{
if
(
$rule
instanceof
Less_Tree_Declaration
&&
$rule
->
variable
)
{
$variables
[
$rule
->
name
]
=
$this
->
getVariableValue
(
$rule
);
}
}
return
$variables
;
}
public
function
findVarByName
(
$var_name
)
{
$rules
=
$this
->
cachedEvaldRules
??
$this
->
rules
;
foreach
(
$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 Less_Tree $var
* @return mixed
* @phan-return string|float|array<string|float>
*/
private
function
getVariableValue
(
Less_Tree
$var
)
{
switch
(
get_class
(
$var
)
)
{
case
Less_Tree_Color
::
class
:
return
$this
->
rgb2html
(
$var
->
rgb
);
case
Less_Tree_Variable
::
class
:
return
$this
->
findVarByName
(
$var
->
name
);
case
Less_Tree_Keyword
::
class
:
return
$var
->
value
;
case
Less_Tree_Anonymous
::
class
:
$return
=
[];
if
(
is_array
(
$var
->
value
)
)
{
foreach
(
$var
->
value
as
$value
)
{
/** @var Less_Tree $value */
// in compilation phase, Less_Tree_Anonymous::$val can be a Less_Tree[]
// @phan-suppress-next-line PhanTypeExpectedObjectPropAccess,PhanTypeMismatchArgument
$return
[
$value
->
name
]
=
$this
->
getVariableValue
(
$value
);
}
}
return
count
(
$return
)
===
1
?
$return
[
0
]
:
$return
;
case
Less_Tree_Url
::
class
:
// Based on Less_Tree_Url::genCSS()
// Recurse to serialize the Less_Tree_Quoted value
return
'url('
.
$this
->
getVariableValue
(
$var
->
value
)
.
')'
;
case
Less_Tree_Declaration
::
class
:
if
(
$var
->
value
instanceof
Less_Tree_Anonymous
)
{
$nodes
=
$this
->
parseNode
(
$var
->
value
->
value
,
[
'value'
,
'important'
],
0
,
[]
);
return
$this
->
getVariableValue
(
$nodes
[
1
][
0
]
);
}
return
$this
->
getVariableValue
(
$var
->
value
);
case
Less_Tree_Value
::
class
:
$values
=
[];
foreach
(
$var
->
value
as
$sub_value
)
{
$values
[]
=
$this
->
getVariableValue
(
$sub_value
);
}
return
count
(
$values
)
===
1
?
$values
[
0
]
:
$values
;
case
Less_Tree_Quoted
::
class
:
return
$var
->
quote
.
$var
->
value
.
$var
->
quote
;
case
Less_Tree_Dimension
::
class
:
$value
=
$var
->
value
;
if
(
$var
->
unit
&&
$var
->
unit
->
numerator
)
{
$value
.=
$var
->
unit
->
numerator
[
0
];
}
return
$value
;
case
Less_Tree_Expression
::
class
:
$values
=
[];
foreach
(
$var
->
value
as
$item
)
{
$values
[]
=
$this
->
getVariableValue
(
$item
);
}
return
implode
(
' '
,
$values
);
case
Less_Tree_Operation
::
class
:
throw
new
Exception
(
'getVariables() require Less to be compiled. please use $parser->getCss() before calling getVariables()'
);
case
Less_Tree_Unit
::
class
:
case
Less_Tree_Comment
::
class
:
case
Less_Tree_Import
::
class
:
case
Less_Tree_Ruleset
::
class
:
default
:
throw
new
Exception
(
"type missing in switch/case getVariableValue for "
.
get_class
(
$var
)
);
}
}
private
function
rgb2html
(
$r
,
$g
=
-
1
,
$b
=
-
1
)
{
if
(
is_array
(
$r
)
&&
count
(
$r
)
==
3
)
{
[
$r
,
$g
,
$b
]
=
$r
;
}
return
sprintf
(
'#%02x%02x%02x'
,
$r
,
$g
,
$b
);
}
/**
* Run pre-compile visitors
*/
private
function
PreVisitors
(
$root
)
{
if
(
self
::
$options
[
'plugins'
]
)
{
foreach
(
self
::
$options
[
'plugins'
]
as
$plugin
)
{
if
(
!
empty
(
$plugin
->
isPreEvalVisitor
)
)
{
$plugin
->
run
(
$root
);
}
}
}
}
/**
* Run post-compile visitors
*/
private
function
PostVisitors
(
$evaldRoot
)
{
$visitors
=
[];
$visitors
[]
=
new
Less_Visitor_joinSelector
();
if
(
self
::
$has_extends
)
{
$visitors
[]
=
new
Less_Visitor_processExtends
();
}
$visitors
[]
=
new
Less_Visitor_toCSS
();
if
(
self
::
$options
[
'plugins'
]
)
{
foreach
(
self
::
$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
*
* @throws Less_Exception_Parser If the compiler encounters invalid syntax
* @param string $str The string to convert
* @param string|null $file_uri The url of the file
* @return $this
*/
public
function
parse
(
$str
,
$file_uri
=
null
)
{
if
(
!
$file_uri
)
{
$uri_root
=
''
;
$filename
=
'anonymous-file-'
.
self
::
$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 If the compiler encounters invalid syntax
* @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|$this
*/
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
);
$this
->
env
->
addParsedFile
(
$filename
);
if
(
$returnRoot
)
{
$rules
=
$this
->
GetRules
(
$filename
);
$return
=
new
Less_Tree_Ruleset
(
null
,
$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 $this
*/
public
function
ModifyVars
(
$vars
)
{
$this
->
input
=
self
::
serializeVars
(
$vars
);
$this
->
_parse
();
return
$this
;
}
/**
* @param string $filename
* @param string $uri_root
*/
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
=
[];
// 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
*
* Import closures are called with a single `$path` argument containing the unquoted `@import`
* string an input LESS file. The string is unchanged, except for a statically appended ".less"
* suffix if the basename does not yet contain a dot. If a dot is present in the filename, you
* are responsible for choosing whether to expand "foo.bar" to "foo.bar.less". If your callback
* can handle this import statement, return an array with an absolute file path and an optional
* URI path, or return void/null to indicate that your callback does not handle this import
* statement.
*
* Example:
*
* function ( $path ) {
* if ( $path === 'virtual/something.less' ) {
* return [ '/srv/elsewhere/thing.less', null ];
* }
* }
*
* @param array $dirs The key should be a server directory from which LESS
* files may be imported. The value is an optional public URL or URL base path that corresponds to
* the same directory (use empty string otherwise). The value may also be a closure, in
* which case the key is ignored.
* @phan-param array<string,string|callable> $dirs
*/
public
function
SetImportDirs
(
$dirs
)
{
self
::
$options
[
'import_dirs'
]
=
[];
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
,
'/'
)
.
'/'
;
}
}
self
::
$options
[
'import_dirs'
][
$path
]
=
$uri_root
;
}
}
/**
* @param string|null $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
(
self
::
$options
[
'cache_method'
]
==
'callback'
)
{
$callback
=
self
::
$options
[
'cache_callback_get'
];
if
(
is_callable
(
$callback
)
)
{
$cache
=
$callback
(
$this
,
$file_path
,
$cache_file
);
if
(
$cache
)
{
$this
->
unsetInput
();
return
$cache
;
}
}
}
elseif
(
file_exists
(
$cache_file
)
)
{
switch
(
self
::
$options
[
'cache_method'
]
)
{
// Using serialize
case
'serialize'
:
$cache
=
unserialize
(
file_get_contents
(
$cache_file
)
);
if
(
$cache
)
{
touch
(
$cache_file
);
$this
->
unsetInput
();
return
$cache
;
}
break
;
}
}
}
$this
->
skipWhitespace
(
0
);
$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
(
self
::
$options
[
'cache_method'
]
==
'callback'
)
{
$callback
=
self
::
$options
[
'cache_callback_set'
];
if
(
is_callable
(
$callback
)
)
{
$callback
(
$this
,
$file_path
,
$cache_file
,
$rules
);
}
}
else
{
switch
(
self
::
$options
[
'cache_method'
]
)
{
case
'serialize'
:
file_put_contents
(
$cache_file
,
serialize
(
$rules
)
);
break
;
}
Less_Cache
::
CleanCache
();
}
}
return
$rules
;
}
/**
* @internal since 4.3.0 No longer a public API.
*/
private
function
setInput
(
$file_path
)
{
// Set up the input buffer
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
\x
EF
\x
BB
\x
BF/'
,
''
,
$this
->
input
);
$this
->
input_len
=
strlen
(
$this
->
input
);
if
(
self
::
$options
[
'sourceMap'
]
&&
$this
->
env
->
currentFileInfo
)
{
$uri
=
$this
->
env
->
currentFileInfo
[
'currentUri'
];
self
::
$contentsMap
[
$uri
]
=
$this
->
input
;
}
}
/**
* @internal since 4.3.0 No longer a public API.
*/
private
function
unsetInput
()
{
// Free up some memory
$this
->
input
=
$this
->
pos
=
$this
->
input_len
=
$this
->
furthest
=
null
;
$this
->
saveStack
=
[];
}
/**
* @internal since 4.3.0 Use Less_Cache instead.
*/
private
function
cacheFile
(
$file_path
)
{
if
(
$file_path
&&
$this
->
CacheEnabled
()
)
{
$env
=
get_object_vars
(
$this
->
env
);
unset
(
$env
[
'frames'
]
);
$parts
=
[];
$parts
[]
=
$file_path
;
$parts
[]
=
filesize
(
$file_path
);
$parts
[]
=
filemtime
(
$file_path
);
$parts
[]
=
$env
;
$parts
[]
=
Less_Version
::
cache_version
;
$parts
[]
=
self
::
$options
[
'cache_method'
];
return
Less_Cache
::
$cache_dir
.
Less_Cache
::
$prefix
.
base_convert
(
sha1
(
json_encode
(
$parts
)
),
16
,
36
)
.
'.lesscache'
;
}
}
/**
* @since 4.3.0
* @return string[]
*/
public
function
getParsedFiles
()
{
return
$this
->
env
->
imports
;
}
/**
* @internal since 4.3.0 No longer a public API.
*/
private
function
save
()
{
$this
->
saveStack
[]
=
$this
->
pos
;
}
private
function
restore
()
{
if
(
$this
->
pos
>
$this
->
furthest
)
{
$this
->
furthest
=
$this
->
pos
;
}
$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
)
{
// @phan-suppress-next-line PhanParamSuspiciousOrder False positive
return
strpos
(
"
\t\n\r\v\f
"
,
$this
->
input
[
$this
->
pos
+
$offset
]
)
!==
false
;
}
/**
* Match a single character in the input.
*
* @param string $tok
* @return string|null
* @see less-2.5.3.js#parserInput.$char
*/
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
*
* @return string|array|null
* @see less-2.5.3.js#parserInput.$re
*/
private
function
matchReg
(
$tok
)
{
if
(
preg_match
(
$tok
,
$this
->
input
,
$match
,
0
,
$this
->
pos
)
)
{
$this
->
skipWhitespace
(
strlen
(
$match
[
0
]
)
);
return
count
(
$match
)
===
1
?
$match
[
0
]
:
$match
;
}
}
/**
* Match an exact string of characters.
*
* @param string $tok
* @return string|null
* @see less-2.5.3.js#parserInput.$str
*/
private
function
matchStr
(
$tok
)
{
$tokLength
=
strlen
(
$tok
);
if
(
(
$this
->
pos
<
$this
->
input_len
)
&&
substr
(
$this
->
input
,
$this
->
pos
,
$tokLength
)
===
$tok
)
{
$this
->
skipWhitespace
(
$tokLength
);
return
$tok
;
}
}
/**
* @param int|null $loc
* @return array|string|void|null
* @see less-3.13.1.js#parserInput.$quoted
*/
private
function
parseQuoted
(
$loc
=
null
)
{
$pos
=
$loc
??
$this
->
pos
;
$startChar
=
$this
->
input
[
$pos
]
??
''
;
if
(
$startChar
!==
'
\'
'
&&
$startChar
!==
'"'
)
{
return
;
}
$currentPos
=
$pos
;
$i
=
1
;
while
(
$currentPos
+
$i
<
$this
->
input_len
)
{
// Optimization: Skip over irrelevant chars without slow loop
$i
+=
strcspn
(
$this
->
input
,
"
\n\r
$startChar
\\
"
,
$currentPos
+
$i
);
switch
(
$this
->
input
[
$currentPos
+
$i
++]
)
{
case
"
\\
"
:
$i
++;
break
;
case
"
\r
"
:
case
"
\n
"
:
break
;
case
$startChar
:
// NOTE: Our optimization means we look ahead instead of behind,
// so no +1s here.
$str
=
substr
(
$this
->
input
,
$currentPos
,
$i
);
if
(
!
$loc
&&
$loc
!==
0
)
{
$this
->
skipWhitespace
(
$i
);
return
$str
;
}
return
[
$startChar
,
$str
];
}
}
return
null
;
}
/**
* Permissive parsing. Ignores everything except matching {} [] () and quotes
* until matching token (outside of blocks)
* @see less-3.13.1.js#parserInput.$parseUntil
*/
private
function
parseUntil
(
$tok
)
{
$quote
=
''
;
$returnVal
=
null
;
$inComment
=
false
;
$blockDepth
=
0
;
$blockStack
=
[];
$parseGroups
=
[];
$startPos
=
$this
->
pos
;
$lastPos
=
$this
->
pos
;
$i
=
$this
->
pos
;
$loop
=
true
;
if
(
is_string
(
$tok
)
)
{
$testChar
=
static
function
(
$char
)
use
(
$tok
)
{
return
$tok
===
$char
;
};
}
else
{
$testChar
=
static
function
(
$char
)
use
(
$tok
)
{
return
in_array
(
$char
,
$tok
);
};
}
do
{
$nextChar
=
$this
->
input
[
$i
];
if
(
$blockDepth
===
0
&&
$testChar
(
$nextChar
)
)
{
$returnVal
=
substr
(
$this
->
input
,
$lastPos
,
$i
-
$lastPos
);
if
(
$returnVal
)
{
$parseGroups
[]
=
$returnVal
;
}
else
{
$parseGroups
[]
=
' '
;
}
$returnVal
=
$parseGroups
;
$this
->
skipWhitespace
(
$i
-
$startPos
);
$loop
=
false
;
}
else
{
if
(
$inComment
)
{
if
(
$nextChar
===
'*'
&&
(
$this
->
input
[
$i
+
1
]
??
''
)
===
'/'
)
{
$i
++;
$blockDepth
--;
$inComment
=
false
;
}
$i
++;
continue
;
}
switch
(
$nextChar
)
{
case
'
\\
'
:
$i
++;
$nextChar
=
$this
->
input
[
$i
]
??
''
;
$parseGroups
[]
=
substr
(
$this
->
input
,
$lastPos
,
$i
-
$lastPos
+
1
);
$lastPos
=
$i
+
1
;
break
;
case
'/'
:
if
(
(
$this
->
input
[
$i
+
1
]
??
''
)
===
'*'
)
{
$i
++;
$inComment
=
true
;
$blockDepth
++;
}
break
;
case
'
\'
'
:
case
'"'
:
$quote
=
$this
->
parseQuoted
(
$i
);
if
(
$quote
)
{
$parseGroups
[]
=
substr
(
$this
->
input
,
$lastPos
,
$i
-
$lastPos
);
$parseGroups
[]
=
$quote
;
$i
+=
strlen
(
$quote
[
1
]
)
-
1
;
$lastPos
=
$i
+
1
;
}
else
{
$this
->
skipWhitespace
(
$i
-
$startPos
);
$returnVal
=
$nextChar
;
$loop
=
false
;
}
break
;
case
'{'
:
$blockStack
[]
=
'}'
;
$blockDepth
++;
break
;
case
'('
:
$blockStack
[]
=
')'
;
$blockDepth
++;
break
;
case
'['
:
$blockStack
[]
=
']'
;
$blockDepth
++;
break
;
case
'}'
:
case
')'
:
case
']'
:
$expected
=
array_pop
(
$blockStack
);
if
(
$nextChar
===
$expected
)
{
$blockDepth
--;
}
else
{
// move the parser to the error and return expected;
$this
->
skipWhitespace
(
$i
-
$startPos
);
$returnVal
=
$expected
;
$loop
=
false
;
}
}
$i
++;
if
(
$i
>
$this
->
input_len
)
{
$loop
=
false
;
}
}
}
while
(
$loop
);
return
$returnVal
?:
null
;
}
/**
* Same as match(), but don't change the state of the parser,
* just return the match.
*
* @param string $tok
* @return int|false
*/
private
function
peekReg
(
$tok
)
{
return
preg_match
(
$tok
,
$this
->
input
,
$match
,
0
,
$this
->
pos
);
}
/**
* @param string $tok
*/
private
function
peekChar
(
$tok
)
{
return
(
$this
->
pos
<
$this
->
input_len
)
&&
(
$this
->
input
[
$this
->
pos
]
===
$tok
);
}
/**
* @param int $length
* @see less-2.5.3.js#skipWhitespace
*/
private
function
skipWhitespace
(
$length
)
{
$this
->
pos
+=
$length
;
for
(
;
$this
->
pos
<
$this
->
input_len
;
$this
->
pos
++
)
{
$currentChar
=
$this
->
input
[
$this
->
pos
];
if
(
$this
->
autoCommentAbsorb
&&
$currentChar
===
'/'
)
{
$nextChar
=
$this
->
input
[
$this
->
pos
+
1
]
??
''
;
if
(
$nextChar
===
'/'
)
{
$comment
=
[
'index'
=>
$this
->
pos
,
'isLineComment'
=>
true
];
$nextNewLine
=
strpos
(
$this
->
input
,
"
\n
"
,
$this
->
pos
+
2
);
if
(
$nextNewLine
===
false
)
{
$nextNewLine
=
$this
->
input_len
??
0
;
}
$this
->
pos
=
$nextNewLine
;
$comment
[
'text'
]
=
substr
(
$this
->
input
,
$this
->
pos
,
$nextNewLine
-
$this
->
pos
);
$this
->
commentStore
[]
=
$comment
;
continue
;
}
elseif
(
$nextChar
===
'*'
)
{
$nextStarSlash
=
strpos
(
$this
->
input
,
"*/"
,
$this
->
pos
+
2
);
if
(
$nextStarSlash
!==
false
)
{
$comment
=
[
'index'
=>
$this
->
pos
,
'text'
=>
substr
(
$this
->
input
,
$this
->
pos
,
$nextStarSlash
+
2
-
$this
->
pos
),
'isLineComment'
=>
false
,
];
$this
->
pos
+=
strlen
(
$comment
[
'text'
]
)
-
1
;
$this
->
commentStore
[]
=
$comment
;
continue
;
}
}
break
;
}
// Optimization: Skip over irrelevant chars without slow loop
$skip
=
strspn
(
$this
->
input
,
"
\n\t\r
"
,
$this
->
pos
);
if
(
$skip
)
{
$this
->
pos
+=
$skip
-
1
;
}
if
(
!
$skip
&&
$this
->
pos
<
$this
->
input_len
)
{
break
;
}
}
}
/**
* Parse a token from a regexp or method name string
*
* @param string $tok
* @param string|null $msg
* @see less-2.5.3.js#Parser.expect
*/
private
function
expect
(
$tok
,
$msg
=
null
)
{
if
(
$tok
[
0
]
===
'/'
)
{
$result
=
$this
->
matchReg
(
$tok
);
}
else
{
$result
=
$this
->
$tok
();
}
if
(
$result
!==
null
)
{
return
$result
;
}
$this
->
Error
(
$msg
?
"Expected '"
.
$tok
.
"' got '"
.
$this
->
input
[
$this
->
pos
]
.
"'"
:
$msg
);
}
/**
* @param string $tok
* @param string|null $msg
*/
private
function
expectChar
(
$tok
,
$msg
=
null
)
{
$result
=
$this
->
matchChar
(
$tok
);
if
(
!
$result
)
{
$msg
=
$msg
?:
"Expected '"
.
$tok
.
"' got '"
.
$this
->
input
[
$this
->
pos
]
.
"'"
;
$this
->
Error
(
$msg
);
}
else
{
return
$result
;
}
}
/**
* @param string $str
* @see less-3.13.1.js#ParserInput.start
*/
private
function
parserInputStart
(
$str
)
{
$this
->
pos
=
$this
->
furthest
=
0
;
$this
->
input
=
$str
;
$this
->
input_len
=
strlen
(
$str
);
$this
->
skipWhitespace
(
0
);
}
/**
* Used after initial parsing to create nodes on the fly
*
* @param string $str string to parse
* @param string[] $parseList array of parsers to run input through e.g. ["value", "important"]
* @param int $currentIndex start number to begin indexing
* @param array $fileInfo fileInfo to attach to created nodes
* @return array
* @see less-3.13.1.js#Parser.parseNode
*/
public
function
parseNode
(
$str
,
array
$parseList
,
$currentIndex
,
$fileInfo
)
{
$returnNodes
=
[];
try
{
$this
->
parserInputStart
(
$str
);
foreach
(
$parseList
as
$p
)
{
$i
=
$this
->
pos
;
$method
=
'parse'
.
ucfirst
(
$p
);
if
(
!
method_exists
(
$this
,
$method
)
)
{
throw
new
CompileError
(
'Unknown parser '
.
$p
);
}
$result
=
$this
->
$method
();
if
(
$result
)
{
$result
->
index
=
$i
+
$currentIndex
;
$result
->
currentFileInfo
=
$fileInfo
;
$returnNodes
[]
=
$result
;
}
else
{
$returnNodes
[]
=
null
;
}
}
if
(
$this
->
pos
>=
$this
->
input_len
)
{
return
[
null
,
$returnNodes
];
}
else
{
return
[
true
,
null
];
}
}
catch
(
Less_Exception_Parser
$e
)
{
throw
new
Less_Exception_Parser
(
$e
->
getMessage
(),
$e
,
(
$e
->
index
??
0
)
+
$currentIndex
,
$fileInfo
);
}
}
//
// Here in, the parsing rules/functions
//
// The basic structure of the syntax tree generated is as follows:
//
// Ruleset -> Declaration -> 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', [
// Declaration ("color", Value ([Expression [Color #fff]]))
// Declaration ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]]))
// Declaration ("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 | declaration )+
// ruleset → selector+ block
// block → '{' primary '}'
//
// Only at one point is the primary rule not called from the
// block rule: at the root level.
//
// @see less-2.5.3.js#parsers.primary
private
function
parsePrimary
()
{
$root
=
[];
while
(
true
)
{
while
(
true
)
{
$node
=
$this
->
parseComment
();
if
(
!
$node
)
{
break
;
}
$root
[]
=
$node
;
}
// always process comments before deciding if finished
if
(
$this
->
pos
>=
$this
->
input_len
)
{
break
;
}
if
(
$this
->
peekChar
(
'}'
)
)
{
break
;
}
$node
=
$this
->
parseExtend
(
true
);
if
(
$node
)
{
$root
=
array_merge
(
$root
,
$node
);
continue
;
}
$node
=
$this
->
parseMixinDefinition
()
// Optimisation: NameValue is specific to less.php
/**
* TODO enabling $this->parseNameValue causes property-accessors to fail with
*
* 'error evaluating function `lighten` The first argument to lighten must be a
* color index: 146 in property-accessors.less on line 9,
*
* note: the Less_Tree_NameValue specifies that it may break color keyword
* interpretation
*/
// ?? $this->parseNameValue()
??
$this
->
parseDeclaration
()
??
$this
->
parseRuleset
()
??
$this
->
parseMixinCall
(
false
,
false
)
??
$this
->
parseVariableCall
()
??
$this
->
parseAtRule
();
if
(
$node
)
{
$root
[]
=
$node
;
}
elseif
(
!
$this
->
matchReg
(
'/
\\
G[
\s\n
;]+/'
)
)
{
break
;
}
}
return
$root
;
}
/**
* comments are collected by the main parsing mechanism and then assigned to nodes
* where the current structure allows it
*
* @return Less_Tree_Comment|void
* @see less-2.5.3.js#parsers.comment
*/
private
function
parseComment
()
{
$comment
=
array_shift
(
$this
->
commentStore
);
if
(
$comment
)
{
return
new
Less_Tree_Comment
(
$comment
[
'text'
],
$comment
[
'isLineComment'
],
$comment
[
'index'
],
$this
->
env
->
currentFileInfo
);
}
}
/**
* @see less-3.13.1.js#parsers.entities.mixinLookup
*/
private
function
parseEntitiesMixinLookup
()
{
return
$this
->
parseMixinCall
(
true
,
true
);
}
/**
* A string, which supports escaping " and '
*
* "milky way" 'he\'s the one!'
*
* @return Less_Tree_Quoted|null
* @see less-3.13.1.js#entities.quoted
*/
private
function
parseEntitiesQuoted
(
$forceEscaped
=
false
)
{
// Optimization: Inline matchChar() here, with its skipWhitespace(1) call below
$isEscaped
=
(
$this
->
input
[
$this
->
pos
]
??
null
)
===
'~'
;
$index
=
$this
->
pos
;
if
(
$forceEscaped
&&
!
$isEscaped
)
{
return
;
}
// Optimization: Move save() down to avoid save()+restore()
// overhead during the early return above which is a hot code path.
$this
->
save
();
if
(
$isEscaped
)
{
$this
->
skipWhitespace
(
1
);
}
$str
=
$this
->
parseQuoted
();
if
(
!
$str
)
{
$this
->
restore
();
return
;
}
$this
->
forget
();
return
new
Less_Tree_Quoted
(
$str
[
0
],
substr
(
$str
,
1
,
-
1
),
$isEscaped
,
$index
,
$this
->
env
->
currentFileInfo
);
}
/**
* A catch-all word, such as:
*
* black border-collapse
*
* @return Less_Tree_Keyword|Less_Tree_Color|null
*/
private
function
parseEntitiesKeyword
()
{
// $k = $this->matchReg('/\\G\\[?(?:[\\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+\\]?/');
$k
=
$this
->
matchReg
(
'/
\\
G%|
\\
G
\\
[?(?:[
\\
w-]|
\\\\
(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+
\\
]?/'
);
if
(
$k
)
{
$color
=
Less_Tree_Color
::
fromKeyword
(
$k
);
if
(
$color
)
{
return
$color
;
}
return
new
Less_Tree_Keyword
(
$k
);
}
}
//
// 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.
//
// @see less-2.5.3.js#parsers.entities.call
private
function
parseEntitiesCall
()
{
$index
=
$this
->
pos
;
if
(
$this
->
peekReg
(
'/
\\
Gurl
\(
/i'
)
)
{
return
;
}
$this
->
save
();
$name
=
$this
->
matchReg
(
'/
\\
G([
\w
-]+|%|progid:[
\w\.
]+)
\(
/'
);
if
(
!
$name
)
{
$this
->
forget
();
return
;
}
$name
=
$name
[
1
];
$nameLC
=
strtolower
(
$name
);
if
(
$nameLC
===
'alpha'
)
{
$alpha_ret
=
$this
->
parseAlpha
();
if
(
$alpha_ret
)
{
return
$alpha_ret
;
}
}
$args
=
$this
->
parseEntitiesArguments
();
if
(
!
$this
->
matchChar
(
')'
)
)
{
$this
->
restore
();
return
;
}
$this
->
forget
();
return
new
Less_Tree_Call
(
$name
,
$args
,
$index
,
$this
->
env
->
currentFileInfo
);
}
/**
* Parse a list of arguments
*
* @return array<Less_Tree_Assignment|Less_Tree_Expression>
*/
private
function
parseEntitiesArguments
()
{
$args
=
[];
while
(
true
)
{
$arg
=
$this
->
parseEntitiesAssignment
()
??
$this
->
parseExpression
();
if
(
!
$arg
)
{
break
;
}
$args
[]
=
$arg
;
if
(
!
$this
->
matchChar
(
','
)
)
{
break
;
}
}
return
$args
;
}
/** @return Less_Tree_Dimension|Less_Tree_Color|Less_Tree_Quoted|Less_Tree_UnicodeDescriptor|null */
private
function
parseEntitiesLiteral
()
{
return
$this
->
parseEntitiesDimension
()
??
$this
->
parseEntitiesColor
()
??
$this
->
parseEntitiesQuoted
()
??
$this
->
parseUnicodeDescriptor
();
}
/**
* Assignments are argument entities for calls.
*
* They are present in IE filter properties as shown below.
*
* filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* )
*
* @return Less_Tree_Assignment|null
* @see less-2.5.3.js#parsers.entities.assignment
*/
private
function
parseEntitiesAssignment
()
{
$key
=
$this
->
matchReg
(
'/
\\
G
\w
+(?=
\s
?=)/'
);
if
(
!
$key
)
{
return
;
}
if
(
!
$this
->
matchChar
(
'='
)
)
{
return
;
}
$value
=
$this
->
parseEntity
();
if
(
$value
)
{
return
new
Less_Tree_Assignment
(
$key
,
$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
()
{
$char
=
$this
->
input
[
$this
->
pos
]
??
null
;
$this
->
autoCommentAbsorb
=
false
;
// Optimisation: 'u' check is specific to less.php
if
(
$char
!==
'u'
||
!
$this
->
matchReg
(
'/
\\
Gurl
\(
/'
)
)
{
$this
->
autoCommentAbsorb
=
true
;
return
;
}
$value
=
$this
->
parseEntitiesQuoted
()
??
$this
->
parseEntitiesVariable
()
??
$this
->
parseEntitiesProperty
()
??
$this
->
matchReg
(
'/
\\
Gdata
\:
.*?[^
\)
]+/'
)
// TODO less doesn't handle this
??
$this
->
matchReg
(
'/
\\
G(?:(?:
\\\\
[
\(\)\'
"])|[^
\(\)\'
"])+/'
)
??
null
;
if
(
!
$value
)
{
$value
=
''
;
}
$this
->
autoCommentAbsorb
=
true
;
$this
->
expectChar
(
')'
);
if
(
$value
instanceof
Less_Tree_Quoted
||
$value
instanceof
Less_Tree_Variable
||
$value
instanceof
Less_Tree_Property
)
{
return
new
Less_Tree_Url
(
$value
,
$this
->
env
->
currentFileInfo
);
}
return
new
Less_Tree_Url
(
new
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`.
*
* @return Less_Tree_Variable|Less_Tree_VariableCall|Less_Tree_NamespaceValue|null
* @see less-3.13.1.js#parsers.entities.variable
*/
private
function
parseEntitiesVariable
()
{
$index
=
$this
->
pos
;
$this
->
save
();
if
(
$this
->
peekChar
(
'@'
)
)
{
$name
=
$this
->
matchReg
(
'/
\\
G@@?[
\w
-]+/'
);
if
(
$name
)
{
$ch
=
$this
->
input
[
$this
->
pos
]
??
''
;
$prevChar
=
$this
->
input
[
$this
->
pos
-
1
]
??
''
;
if
(
$ch
===
'('
||
(
$ch
===
'['
&&
!
preg_match
(
'/
\s
/'
,
$prevChar
,
$match
)
)
)
{
// this may be a VariableCall lookup
$result
=
$this
->
parseVariableCall
(
$name
);
if
(
$result
)
{
$this
->
forget
();
return
$result
;
}
}
$this
->
forget
();
return
new
Less_Tree_Variable
(
$name
,
$index
,
$this
->
env
->
currentFileInfo
);
}
}
$this
->
restore
();
}
/**
* A variable entity using the protective `{}` e.g. `@{var}`.
*
* @return Less_Tree_Variable|null
*/
private
function
parseEntitiesVariableCurly
()
{
$index
=
$this
->
pos
;
if
(
$this
->
input_len
>
(
$this
->
pos
+
1
)
&&
$this
->
input
[
$this
->
pos
]
===
'@'
)
{
$curly
=
$this
->
matchReg
(
'/
\\
G@
\{
([
\w
-]+)
\}
/'
);
if
(
$curly
)
{
return
new
Less_Tree_Variable
(
'@'
.
$curly
[
1
],
$index
,
$this
->
env
->
currentFileInfo
);
}
}
}
/**
* A Property accessor, such as `$color`, in
*
* background-color: $color
*/
private
function
parseEntitiesProperty
()
{
$index
=
$this
->
pos
;
if
(
(
$this
->
input
[
$this
->
pos
]
??
''
)
===
'$'
)
{
$name
=
$this
->
matchReg
(
'/
\\
G
\$
[
\w
-]+/'
);
if
(
$name
)
{
return
new
Less_Tree_Property
(
$name
,
$index
,
$this
->
env
->
currentFileInfo
);
}
}
}
// A property entity useing the protective {} e.g. @{prop}
private
function
parseEntitiesPropertyCurly
()
{
$index
=
$this
->
pos
;
if
(
$this
->
input
[
$this
->
pos
]
===
'$'
)
{
$curly
=
$this
->
matchReg
(
'/
\\
G@
\{
([
\w
-]+)
\}
/'
);
if
(
$curly
)
{
return
new
Less_Tree_Property
(
"$"
.
$curly
[
1
],
$index
,
$this
->
env
->
currentFileInfo
);
}
}
}
/**
* A Hexadecimal color
*
* #4F3C2F
*
* `rgb` and `hsl` colors are parsed through the `entities.call` parser.
*
* @return Less_Tree_Color|null
*/
private
function
parseEntitiesColor
()
{
if
(
$this
->
peekChar
(
'#'
)
)
{
$rgb
=
$this
->
matchReg
(
'/
\\
G#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/'
);
if
(
$rgb
)
{
return
new
Less_Tree_Color
(
$rgb
[
1
],
1
,
null
,
$rgb
[
0
]
);
}
}
}
/**
* A Dimension, that is, a number and a unit
*
* 0.5em 95%
*
* @return Less_Tree_Dimension|null
*/
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]+)?/i'
);
if
(
$value
)
{
if
(
isset
(
$value
[
2
]
)
)
{
return
new
Less_Tree_Dimension
(
$value
[
1
],
$value
[
2
]
);
}
return
new
Less_Tree_Dimension
(
$value
[
1
]
);
}
}
/**
* A unicode descriptor, as is used in unicode-range
*
* U+0?? or U+00A1-00A9
*
* @return Less_Tree_UnicodeDescriptor|null
*/
private
function
parseUnicodeDescriptor
()
{
// Optimization: Hardcode first char, to avoid matchReg() cost for common case
$char
=
$this
->
input
[
$this
->
pos
]
??
null
;
if
(
$char
!==
'U'
)
{
return
;
}
$ud
=
$this
->
matchReg
(
'/
\\
G(U
\+
[0-9a-fA-F?]+)(
\-
[0-9a-fA-F?]+)?/'
);
if
(
$ud
)
{
return
new
Less_Tree_UnicodeDescriptor
(
$ud
[
0
]
);
}
}
/**
* JavaScript code to be evaluated
*
* `window.location.href`
*
* @return Less_Tree_JavaScript|null
* @see less-3.13.1.js#parsers.entities.javascript
*/
private
function
parseEntitiesJavascript
()
{
// Optimization: Hardcode first char, to avoid save()/restore() overhead
// Optimization: Inline matchChar(), with skipWhitespace(1) below
$char
=
$this
->
input
[
$this
->
pos
]
??
null
;
$isEscaped
=
$char
===
'~'
;
if
(
!
$isEscaped
&&
$char
!==
'`'
)
{
return
;
}
$index
=
$this
->
pos
;
$this
->
save
();
if
(
$isEscaped
)
{
$this
->
skipWhitespace
(
1
);
$char
=
$this
->
input
[
$this
->
pos
]
??
null
;
if
(
$char
!==
'`'
)
{
$this
->
restore
();
return
;
}
}
$this
->
skipWhitespace
(
1
);
$js
=
$this
->
matchReg
(
'/
\\
G[^`]*`/'
);
if
(
$js
)
{
$this
->
forget
();
return
new
Less_Tree_JavaScript
(
substr
(
$js
,
0
,
-
1
),
$isEscaped
,
$index
);
}
$this
->
restore
();
}
// The variable part of a variable definition. Used in the `rule` parser
//
// @fink:
//
// @see less-3.13.1.js#parsers.variable
private
function
parseVariable
()
{
if
(
$this
->
peekChar
(
'@'
)
)
{
$name
=
$this
->
matchReg
(
'/
\\
G(@[
\w
-]+)
\s
*:/'
);
if
(
$name
)
{
return
$name
[
1
];
}
}
}
// Call a variable value to retrieve a detached ruleset
// or a value from a detached ruleset's rules.
//
// @fink();
// @fink;
// color: @fink[@color];
//
// @see less-3.13.1.js#parsers.variableCall
private
function
parseVariableCall
(
$parsedName
=
null
)
{
$i
=
$this
->
pos
;
$inValue
=
(
bool
)
$parsedName
;
if
(
$parsedName
===
null
&&
!
$this
->
peekChar
(
'@'
)
)
{
return
;
}
$this
->
save
();
$name
=
$parsedName
??
$this
->
matchReg
(
'/
\\
G(@[
\w
-]+)(
\(\s
*
\)
)?/'
);
if
(
$name
===
null
)
{
$this
->
restore
();
return
;
}
$lookups
=
$this
->
parseMixinRuleLookups
();
if
(
!
$lookups
&&
(
(
$inValue
&&
$this
->
matchStr
(
'()'
)
!==
'()'
)
||
(
(
$name
[
2
]
??
''
)
!==
'()'
)
)
)
{
// Restore error mesage: 'Missing \'[...]\' lookup in variable call'
$this
->
restore
();
return
;
}
if
(
!
$inValue
)
{
$name
=
$name
[
1
];
}
$call
=
new
Less_Tree_VariableCall
(
$name
,
$i
,
$this
->
env
->
currentFileInfo
);
if
(
!
$inValue
&&
$this
->
parseEnd
()
)
{
$this
->
forget
();
return
$call
;
}
else
{
$this
->
forget
();
return
new
Less_Tree_NamespaceValue
(
$call
,
$lookups
,
$i
,
$this
->
env
->
currentFileInfo
);
}
}
//
// extend syntax - used to extend selectors
//
// @see less-2.5.3.js#parsers.extend
private
function
parseExtend
(
$isRule
=
false
)
{
$index
=
$this
->
pos
;
$extendList
=
[];
if
(
!
$this
->
matchStr
(
$isRule
?
'&:extend('
:
':extend('
)
)
{
return
;
}
do
{
$option
=
null
;
$elements
=
[];
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
[]
=
new
Less_Tree_Extend
(
new
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);
// #mixins.square(#fff);
// .rounded(4px, black);
// .button;
//
// We can lookup / return a value using the lookup syntax:
//
// color: #mixin.square(#fff)[@color];
//
// The `while` loop is there because mixins can be
// namespaced, but we only support the child and descendant
// selector for now.
//
// @see less-3.13.1.js#parsers.mixin.call
//
private
function
parseMixinCall
(
$inValue
,
$getLookup
=
null
)
{
$s
=
$this
->
input
[
$this
->
pos
]
??
null
;
$important
=
false
;
$lookups
=
null
;
$index
=
$this
->
pos
;
$args
=
[];
$hasParens
=
false
;
if
(
$s
!==
'.'
&&
$s
!==
'#'
)
{
return
;
}
$this
->
save
();
// stop us absorbing part of an invalid selector
$elements
=
$this
->
parseMixinCallElements
();
if
(
$elements
)
{
if
(
$this
->
matchChar
(
'('
)
)
{
$args
=
(
$this
->
parseMixinArgs
(
true
)
)[
'args'
];
$this
->
expectChar
(
')'
);
$hasParens
=
true
;
}
if
(
$getLookup
!==
false
)
{
$lookups
=
$this
->
parseMixinRuleLookups
();
}
if
(
$getLookup
===
true
&&
$lookups
===
null
)
{
$this
->
restore
();
return
;
}
if
(
$inValue
&&
!
$lookups
&&
!
$hasParens
)
{
// This isn't a valid in-value mixin call
$this
->
restore
();
return
;
}
if
(
!
$inValue
&&
$this
->
parseImportant
()
)
{
$important
=
true
;
}
if
(
$inValue
||
$this
->
parseEnd
()
)
{
$this
->
forget
();
$mixin
=
new
Less_Tree_Mixin_Call
(
$elements
,
$args
,
$index
,
$this
->
env
->
currentFileInfo
,
!
$lookups
&&
$important
);
if
(
$lookups
)
{
return
new
Less_Tree_NamespaceValue
(
$mixin
,
$lookups
);
}
else
{
return
$mixin
;
}
}
}
$this
->
restore
();
}
/**
* Matching elements for mixins
* (Start with . or # and can have > )
* @see less-3.13.1.js#parsers.mixin.elements
*/
private
function
parseMixinCallElements
()
{
$elements
=
[];
$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
[]
=
new
Less_Tree_Element
(
$c
,
$e
,
$elemIndex
,
$this
->
env
->
currentFileInfo
);
$c
=
$this
->
matchChar
(
'>'
);
}
return
$elements
?:
null
;
}
/**
* @param bool $isCall
* @see less-2.5.3.js#parsers.mixin.args
*/
private
function
parseMixinArgs
(
$isCall
)
{
$expressions
=
[];
$argsSemiColon
=
[];
$isSemiColonSeperated
=
null
;
$argsComma
=
[];
$expressionContainsNamed
=
null
;
$name
=
null
;
$returner
=
[
'args'
=>
[],
'variadic'
=>
false
];
$expand
=
false
;
$this
->
save
();
while
(
true
)
{
if
(
$isCall
)
{
$arg
=
$this
->
parseDetachedRuleset
()
??
$this
->
parseExpression
();
}
else
{
$this
->
commentStore
=
[];
if
(
$this
->
input
[
$this
->
pos
]
===
'.'
&&
$this
->
matchStr
(
'...'
)
)
{
$returner
[
'variadic'
]
=
true
;
if
(
$this
->
matchChar
(
";"
)
&&
!
$isSemiColonSeperated
)
{
$isSemiColonSeperated
=
true
;
}
if
(
$isSemiColonSeperated
)
{
$argsSemiColon
[]
=
[
'variadic'
=>
true
];
}
else
{
$argsComma
[]
=
[
'variadic'
=>
true
];
}
break
;
}
$arg
=
$this
->
parseEntitiesVariable
()
??
$this
->
parseEntitiesProperty
()
??
$this
->
parseEntitiesLiteral
()
??
$this
->
parseEntitiesKeyword
();
}
if
(
!
$arg
)
{
break
;
}
$nameLoop
=
null
;
if
(
$arg
instanceof
Less_Tree_Expression
)
{
$arg
->
throwAwayComments
();
}
$value
=
$arg
;
$val
=
null
;
if
(
$isCall
)
{
// Variable
if
(
$value
instanceof
Less_Tree_Expression
&&
count
(
$arg
->
value
)
==
1
)
{
$val
=
$arg
->
value
[
0
];
}
}
else
{
$val
=
$arg
;
}
if
(
$val
instanceof
Less_Tree_Variable
||
$val
instanceof
Less_Tree_Property
)
{
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
=
$this
->
parseDetachedRuleset
()
??
$this
->
parseExpression
();
if
(
!
$value
)
{
if
(
$isCall
)
{
$this
->
Error
(
'could not understand value for named argument'
);
}
else
{
$this
->
restore
();
$returner
[
'args'
]
=
[];
return
$returner
;
}
}
$nameLoop
=
(
$name
=
$val
->
name
);
}
elseif
(
$this
->
matchStr
(
'...'
)
)
{
if
(
!
$isCall
)
{
$returner
[
'variadic'
]
=
true
;
if
(
$this
->
matchChar
(
";"
)
&&
!
$isSemiColonSeperated
)
{
$isSemiColonSeperated
=
true
;
}
if
(
$isSemiColonSeperated
)
{
$argsSemiColon
[]
=
[
'name'
=>
$arg
->
name
,
'variadic'
=>
true
];
}
else
{
$argsComma
[]
=
[
'name'
=>
$arg
->
name
,
'variadic'
=>
true
];
}
break
;
}
else
{
$expand
=
true
;
}
}
elseif
(
!
$isCall
)
{
$name
=
$nameLoop
=
$val
->
name
;
$value
=
null
;
}
}
if
(
$value
)
{
$expressions
[]
=
$value
;
}
$argsComma
[]
=
[
'name'
=>
$nameLoop
,
'value'
=>
$value
,
'expand'
=>
$expand
];
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
=
new
Less_Tree_Value
(
$expressions
);
}
$argsSemiColon
[]
=
[
'name'
=>
$name
,
'value'
=>
$value
,
'expand'
=>
$expand
];
$name
=
null
;
$expressions
=
[];
$expressionContainsNamed
=
false
;
}
}
$this
->
forget
();
$returner
[
'args'
]
=
(
$isSemiColonSeperated
?
$argsSemiColon
:
$argsComma
);
return
$returner
;
}
/**
* @see less-3.13.1.js#parsers.mixin.ruleLookups
*/
private
function
parseMixinRuleLookups
()
{
$lookups
=
[];
if
(
!
$this
->
peekChar
(
'['
)
)
{
return
;
}
while
(
true
)
{
$this
->
save
();
$rule
=
$this
->
parseLookupValue
();
if
(
!
$rule
&&
$rule
!==
''
)
{
$this
->
restore
();
break
;
}
$lookups
[]
=
$rule
;
$this
->
forget
();
}
if
(
$lookups
)
{
return
$lookups
;
}
}
/**
* @see less-3.13.1.js#parsers.mixin.lookupValue
*/
private
function
parseLookupValue
()
{
$this
->
save
();
if
(
!
$this
->
matchChar
(
'['
)
)
{
$this
->
restore
();
return
;
}
$name
=
$this
->
matchReg
(
"/
\\
G(?:[@
\$
]{0,2})[_a-zA-Z0-9-]*/"
);
if
(
!
$this
->
matchChar
(
']'
)
)
{
$this
->
restore
();
return
;
}
if
(
$name
||
$name
===
''
)
{
$this
->
forget
();
return
$name
;
}
$this
->
restore
();
}
//
// 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.
//
// @see less-2.5.3.js#parsers.mixin.definition
private
function
parseMixinDefinition
()
{
$cond
=
null
;
$char
=
$this
->
input
[
$this
->
pos
]
??
null
;
// TODO: Less.js doesn't limit this to $char == '{'.
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
->
restore
();
return
;
}
$this
->
commentStore
=
[];
if
(
$this
->
matchStr
(
'when'
)
)
{
// Guard
$cond
=
$this
->
expect
(
'parseConditions'
,
'Expected conditions'
);
}
$ruleset
=
$this
->
parseBlock
();
if
(
$ruleset
!==
null
)
{
$this
->
forget
();
return
new
Less_Tree_Mixin_Definition
(
$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
->
parseComment
()
??
$this
->
parseEntitiesLiteral
()
??
$this
->
parseEntitiesVariable
()
??
$this
->
parseEntitiesUrl
()
??
$this
->
parseEntitiesProperty
()
??
$this
->
parseEntitiesCall
()
??
$this
->
parseEntitiesKeyword
()
??
$this
->
parseMixinCall
(
true
)
??
$this
->
parseEntitiesJavascript
();
}
//
// A Declaration 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)
//
// @see less-2.5.3.js#parsers.alpha
private
function
parseAlpha
()
{
if
(
!
$this
->
matchReg
(
'/
\\
Gopacity=/i'
)
)
{
return
;
}
$value
=
$this
->
matchReg
(
'/
\\
G[0-9]+/'
);
if
(
$value
===
null
)
{
$value
=
$this
->
expect
(
'parseEntitiesVariable'
,
'Could not parse alpha'
);
}
$this
->
expectChar
(
')'
);
return
new
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 `*`.
*
* @return Less_Tree_Element|null
* @see less-2.5.3.js#parsers.element
*/
private
function
parseElement
()
{
$c
=
$this
->
parseCombinator
();
$index
=
$this
->
pos
;
$e
=
$this
->
matchReg
(
'/
\\
G(?:
\d
+
\.\d
+|
\d
+)%/'
)
??
$this
->
matchReg
(
'/
\\
G(?:[.#]?|:*)(?:[
\w
-]|[^
\x
00-
\x
9f]|
\\\\
(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/'
)
??
$this
->
matchChar
(
'*'
)
??
$this
->
matchChar
(
'&'
)
??
$this
->
parseAttribute
()
??
$this
->
matchReg
(
'/
\\
G
\(
[^&()@]+
\)
/'
)
??
$this
->
matchReg
(
'/
\\
G[
\.
#:](?=@)/'
)
??
$this
->
parseEntitiesVariableCurly
();
if
(
$e
===
null
)
{
$this
->
save
();
if
(
$this
->
matchChar
(
'('
)
)
{
$v
=
$this
->
parseSelector
();
if
(
$v
&&
$this
->
matchChar
(
')'
)
)
{
$e
=
new
Less_Tree_Paren
(
$v
);
$this
->
forget
();
}
else
{
$this
->
restore
();
}
}
else
{
$this
->
forget
();
}
}
if
(
$e
!==
null
)
{
return
new
Less_Tree_Element
(
$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.
//
// @see less-2.5.3.js#parsers.combinator
private
function
parseCombinator
()
{
if
(
$this
->
pos
<
$this
->
input_len
)
{
$c
=
$this
->
input
[
$this
->
pos
];
if
(
$c
===
'/'
)
{
$this
->
save
();
$slashedCombinator
=
$this
->
matchReg
(
'/
\\
G
\/
[a-z]+
\/
/i'
);
if
(
$slashedCombinator
)
{
$this
->
forget
();
return
$slashedCombinator
;
}
$this
->
restore
();
}
// TODO: Figure out why less.js also handles '/' here, and implement with regression test.
if
(
$c
===
'>'
||
$c
===
'+'
||
$c
===
'~'
||
$c
===
'|'
||
$c
===
'^'
)
{
$this
->
pos
++;
if
(
$c
===
'^'
&&
$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
*
* @return Less_Tree_Selector|null
* @see less-2.5.3.js#parsers.lessSelector
*/
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 ::parseElement.
*
* @return Less_Tree_Selector|null
* @see less-2.5.3.js#parsers.selector
*/
private
function
parseSelector
(
$isLess
=
false
)
{
$elements
=
[];
$extendList
=
[];
$condition
=
null
;
$when
=
false
;
$extend
=
false
;
$e
=
null
;
$c
=
null
;
$index
=
$this
->
pos
;
// phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition
while
(
(
$isLess
&&
(
$extend
=
$this
->
parseExtend
()
)
)
||
(
$isLess
&&
(
$when
=
$this
->
matchStr
(
'when'
)
)
)
||
(
$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
new
Less_Tree_Selector
(
$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'
);
}
}
/**
* @return Less_Tree_Attribute|null
* @see less-2.5.3.js#parsers.attribute
*/
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
->
parseEntitiesQuoted
()
??
$this
->
matchReg
(
'/
\\
G[0-9]+%/'
)
??
$this
->
matchReg
(
'/
\\
G[
\w
-]+/'
)
??
$this
->
parseEntitiesVariableCurly
();
}
$this
->
expectChar
(
']'
);
return
new
Less_Tree_Attribute
(
$key
,
$op
,
$val
);
}
/**
* The `block` rule is used by `ruleset` and `mixin.definition`.
* It's a wrapper around the `primary` rule, with added `{}`.
*
* @return array<Less_Tree>|null
* @see less-2.5.3.js#parsers.block
*/
private
function
parseBlock
()
{
if
(
$this
->
matchChar
(
'{'
)
)
{
$content
=
$this
->
parsePrimary
();
if
(
$this
->
matchChar
(
'}'
)
)
{
return
$content
;
}
}
}
private
function
parseBlockRuleset
()
{
$block
=
$this
->
parseBlock
();
if
(
$block
!==
null
)
{
return
new
Less_Tree_Ruleset
(
null
,
$block
);
}
}
/** @return Less_Tree_DetachedRuleset|null */
private
function
parseDetachedRuleset
()
{
$blockRuleset
=
$this
->
parseBlockRuleset
();
if
(
$blockRuleset
)
{
return
new
Less_Tree_DetachedRuleset
(
$blockRuleset
);
}
}
/**
* Ruleset such as:
*
* div, .class, body > p {
* }
*
* @return Less_Tree_Ruleset|null
* @see less-2.5.3.js#parsers.ruleset
*/
private
function
parseRuleset
()
{
$selectors
=
[];
$this
->
save
();
// TODO: missing https://github.com/less/less.js/commit/b8140d4baad18ba732e2b322d8891a9b0ff065d5#diff-cad419f131cbecb0799ee17eba9319d3ff51de09eb3876efb9e4c068c1f6025f
// the commit above updated the `permissive-parse.less` fixture worked on Id36e0f142d7f430603da3f0d6825aa6a0bc9b7f1
// and it required to add an override for permisive-parse.css.
// When working on parse interpolation, please make sure to remove the permissive-parse
// override
while
(
true
)
{
$s
=
$this
->
parseLessSelector
();
if
(
!
$s
)
{
break
;
}
$selectors
[]
=
$s
;
$this
->
commentStore
=
[];
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
->
commentStore
=
[];
}
if
(
$selectors
)
{
$rules
=
$this
->
parseBlock
();
if
(
is_array
(
$rules
)
)
{
$this
->
forget
();
// TODO: Less_Environment::$strictImports is not yet ported
// It is passed here by less.js
return
new
Less_Tree_Ruleset
(
$selectors
,
$rules
);
}
}
// Backtrack
$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
*) *(! *important)?
\s
*([;}])/'
);
if
(
$match
)
{
if
(
$match
[
4
]
==
'}'
)
{
// because we will parse all comments after closing }, we need to reset the store as
// we're going to reset the position to closing }
$this
->
commentStore
=
[];
$this
->
pos
=
$index
+
strlen
(
$match
[
0
]
)
-
1
;
$match
[
2
]
=
rtrim
(
$match
[
2
]
);
}
if
(
$match
[
3
]
)
{
$match
[
2
]
.=
$match
[
3
];
}
$this
->
forget
();
return
new
Less_Tree_NameValue
(
$match
[
1
],
$match
[
2
],
$index
,
$this
->
env
->
currentFileInfo
);
}
$this
->
restore
();
}
// @see less-3.13.1.js#parsers.declaration
private
function
parseDeclaration
()
{
$value
=
null
;
$index
=
$this
->
pos
;
$hasDR
=
false
;
$c
=
$this
->
input
[
$this
->
pos
]
??
null
;
$important
=
null
;
$merge
=
false
;
// TODO: Figure out why less.js also handles ':' here, and implement with regression test.
if
(
$c
===
'.'
||
$c
===
'#'
||
$c
===
'&'
)
{
return
;
}
$this
->
save
();
$name
=
$this
->
parseVariable
()
??
$this
->
parseRuleProperty
();
if
(
$name
)
{
$isVariable
=
is_string
(
$name
);
if
(
$isVariable
)
{
$value
=
$this
->
parseDetachedRuleset
();
if
(
$value
)
{
$hasDR
=
true
;
}
}
$this
->
commentStore
=
[];
if
(
!
$value
)
{
// 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
)
&&
count
(
$name
)
>
1
)
{
$merge
=
array_pop
(
$name
)->
value
;
}
// Custom property values get permissive parsing
if
(
is_array
(
$name
)
&&
array_key_exists
(
0
,
$name
)
// to satisfy phan
&&
$name
[
0
]
instanceof
Less_Tree_Keyword
&&
$name
[
0
]->
value
&&
strpos
(
$name
[
0
]->
value
,
'--'
)
===
0
)
{
$value
=
$this
->
parsePermissiveValue
();
}
else
{
// Try to store values as anonymous
// If we need the value later we'll re-parse it in ruleset.parseValue
$value
=
$this
->
parseAnonymousValue
();
}
if
(
$value
)
{
$this
->
forget
();
// anonymous values absorb the end ';' which is required for them to work
return
new
Less_Tree_Declaration
(
$name
,
$value
,
false
,
$merge
,
$index
,
$this
->
env
->
currentFileInfo
);
}
if
(
!
$value
)
{
$value
=
$this
->
parseValue
();
}
if
(
$value
)
{
$important
=
$this
->
parseImportant
();
}
elseif
(
$isVariable
)
{
$value
=
$this
->
parsePermissiveValue
();
}
}
if
(
$value
&&
(
$this
->
parseEnd
()
||
$hasDR
)
)
{
$this
->
forget
();
return
new
Less_Tree_Declaration
(
$name
,
$value
,
$important
,
$merge
,
$index
,
$this
->
env
->
currentFileInfo
);
}
else
{
$this
->
restore
();
}
}
else
{
$this
->
restore
();
}
}
/**
* @see less-3.13.1.js#parsers.anonymousValue
*/
private
function
parseAnonymousValue
()
{
$index
=
$this
->
pos
;
$match
=
$this
->
matchReg
(
'/
\\
G([^.#@
\$
+
\/\'
"*`(;{}-]*);/'
);
if
(
$match
)
{
return
new
Less_Tree_Anonymous
(
$match
[
1
],
$index
);
}
}
/**
* Used for custom properties, at-rules, and variables (as fallback)
* Parses almost anything inside of {} [] () "" blocks
* until it reaches outer-most tokens.
*
* First, it will try to parse comments and entities to reach
* the end. This is mostly like the Expression parser except no
* math is allowed.
*
* @see less-3.13.1.js#parsers.permissiveValue
* @param null|string|array $untilTokens
*/
private
function
parsePermissiveValue
(
$untilTokens
=
null
)
{
$tok
=
$untilTokens
??
';'
;
$index
=
$this
->
pos
;
$result
=
[];
if
(
is_array
(
$tok
)
)
{
$testCurrentChar
=
static
function
(
$currentChar
)
use
(
$tok
)
{
return
in_array
(
$currentChar
,
$tok
);
};
}
else
{
$testCurrentChar
=
static
function
(
$currentChar
)
use
(
$tok
)
{
return
$tok
===
$currentChar
;
};
}
if
(
$testCurrentChar
(
$this
->
input
[
$this
->
pos
]
)
)
{
return
;
}
$value
=
[];
do
{
$e
=
$this
->
parseComment
();
if
(
$e
)
{
$value
[]
=
$e
;
continue
;
}
$e
=
$this
->
parseEntity
();
if
(
$e
)
{
$value
[]
=
$e
;
}
}
while
(
$e
);
$done
=
$testCurrentChar
(
$this
->
input
[
$this
->
pos
]
);
if
(
$value
)
{
$value
=
new
Less_Tree_Expression
(
$value
);
if
(
$done
)
{
return
$value
;
}
else
{
$result
[]
=
$value
;
}
// Preserve space before $parseUntil as it will not
if
(
$this
->
input
[
$this
->
pos
-
1
]
===
' '
)
{
$result
[]
=
new
Less_Tree_Anonymous
(
' '
,
$index
);
}
}
$this
->
save
();
$value
=
$this
->
parseUntil
(
$tok
);
if
(
$value
)
{
if
(
is_string
(
$value
)
)
{
$this
->
Error
(
"expected '"
.
$value
.
"'"
);
}
if
(
count
(
$value
)
===
1
&&
$value
[
0
]
===
' '
)
{
$this
->
forget
();
return
new
Less_Tree_Anonymous
(
''
,
$index
);
}
$valueLength
=
count
(
$value
);
for
(
$i
=
0
;
$i
<
$valueLength
;
$i
++
)
{
$item
=
$value
[
$i
];
if
(
is_array
(
$item
)
)
{
$result
[]
=
new
Less_Tree_Quoted
(
$item
[
0
],
$item
[
1
],
true
,
$index
,
$this
->
env
->
currentFileInfo
);
}
else
{
if
(
$i
===
$valueLength
-
1
)
{
$item
=
trim
(
$item
);
}
// Treat like quoted values, but replace vars like unquoted expressions
$quote
=
new
Less_Tree_Quoted
(
'
\'
'
,
$item
,
true
,
$index
,
$this
->
env
->
currentFileInfo
);
$quote
->
variableRegex
=
'/@([
\w
-]+)/'
;
$quote
->
propRegex
=
'/
\$
([
\w
-]+)/'
;
$result
[]
=
$quote
;
}
}
$this
->
forget
();
return
new
Less_Tree_Expression
(
$result
,
true
);
}
$this
->
restore
();
}
//
// An @import atrule
//
// @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
->
parseEntitiesQuoted
()
??
$this
->
parseEntitiesUrl
();
if
(
$path
)
{
$features
=
$this
->
parseMediaFeatures
();
if
(
$this
->
matchChar
(
';'
)
)
{
if
(
$features
)
{
$features
=
new
Less_Tree_Value
(
$features
);
}
$this
->
forget
();
return
new
Less_Tree_Import
(
$path
,
$features
,
$options
,
$this
->
pos
,
$this
->
env
->
currentFileInfo
);
}
}
}
$this
->
restore
();
}
private
function
parseImportOptions
()
{
$options
=
[];
// 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
=
[];
do
{
$e
=
$this
->
parseEntitiesKeyword
()
??
$this
->
parseEntitiesVariable
();
if
(
$e
)
{
$nodes
[]
=
$e
;
}
elseif
(
$this
->
matchChar
(
'('
)
)
{
$p
=
$this
->
parseProperty
();
$e
=
$this
->
parseValue
();
if
(
$this
->
matchChar
(
')'
)
)
{
if
(
$p
&&
$e
)
{
$r
=
new
Less_Tree_Declaration
(
$p
,
$e
,
null
,
null
,
$this
->
pos
,
$this
->
env
->
currentFileInfo
,
true
);
$nodes
[]
=
new
Less_Tree_Paren
(
$r
);
}
elseif
(
$e
)
{
$nodes
[]
=
new
Less_Tree_Paren
(
$e
);
}
else
{
return
null
;
}
}
else
{
return
null
;
}
}
}
while
(
$e
);
if
(
$nodes
)
{
return
new
Less_Tree_Expression
(
$nodes
);
}
}
private
function
parseMediaFeatures
()
{
$features
=
[];
do
{
$e
=
$this
->
parseMediaFeature
();
if
(
$e
)
{
$features
[]
=
$e
;
if
(
!
$this
->
matchChar
(
','
)
)
{
break
;
}
}
else
{
$e
=
$this
->
parseEntitiesVariable
();
if
(
$e
)
{
$features
[]
=
$e
;
if
(
!
$this
->
matchChar
(
','
)
)
{
break
;
}
}
}
}
while
(
$e
);
return
$features
?:
null
;
}
/**
* @see less-2.5.3.js#parsers.media
*/
private
function
parseMedia
()
{
if
(
$this
->
matchStr
(
'@media'
)
)
{
$this
->
save
();
$features
=
$this
->
parseMediaFeatures
();
$rules
=
$this
->
parseBlock
();
if
(
$rules
===
null
)
{
$this
->
restore
();
return
;
}
$this
->
forget
();
return
new
Less_Tree_Media
(
$rules
,
$features
,
$this
->
pos
,
$this
->
env
->
currentFileInfo
);
}
}
/**
* A CSS AtRule like `@charset "utf-8";`
*
* @return Less_Tree_Import|Less_Tree_Media|Less_Tree_AtRule|null
* @see less-3.13.1.js#parsers.atrule
* @todo check feature parity with 3.13.1
*/
private
function
parseAtRule
()
{
if
(
!
$this
->
peekChar
(
'@'
)
)
{
return
;
}
$rules
=
null
;
$index
=
$this
->
pos
;
$hasBlock
=
true
;
$hasIdentifier
=
false
;
$hasExpression
=
false
;
$hasUnknown
=
false
;
$isRooted
=
true
;
$value
=
$this
->
parseImport
()
??
$this
->
parseMedia
();
if
(
$value
)
{
return
$value
;
}
$this
->
save
();
$name
=
$this
->
matchReg
(
'/
\\
G@[a-z-]+/'
);
if
(
!
$name
)
{
return
;
}
$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;
isRooted = true;
break;
*/
case
"@counter-style"
:
$hasIdentifier
=
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"
:
$hasUnknown
=
true
;
break
;
case
"@document"
:
case
"@supports"
:
$hasUnknown
=
true
;
$isRooted
=
false
;
break
;
default
:
// TODO: port other parts of https://github.com/less/less.js/commit/e3c13121dfdca48ba8fe26335cc12dd3f7948676
$hasUnknown
=
true
;
break
;
}
$this
->
commentStore
=
[];
if
(
$hasIdentifier
)
{
$value
=
$this
->
parseEntity
();
if
(
!
$value
)
{
$this
->
Error
(
"expected "
.
$name
.
" identifier"
);
}
}
elseif
(
$hasExpression
)
{
$value
=
$this
->
parseExpression
();
if
(
!
$value
)
{
$this
->
Error
(
"expected "
.
$name
.
" expression"
);
}
}
elseif
(
$hasUnknown
)
{
$value
=
$this
->
parsePermissiveValue
(
[
'{'
,
';'
]
);
$hasBlock
=
$this
->
input
[
$this
->
pos
]
===
'{'
;
if
(
!
$value
)
{
if
(
!
$hasBlock
&&
$this
->
input
[
$this
->
pos
]
!==
';'
)
{
$this
->
Error
(
$name
.
" rule is missing block or ending semi-colon"
);
}
}
elseif
(
!
$value
->
value
)
{
$value
=
null
;
}
}
if
(
$hasBlock
)
{
$rules
=
$this
->
parseBlockRuleset
();
}
if
(
$rules
||
(
!
$hasBlock
&&
$value
&&
$this
->
matchChar
(
';'
)
)
)
{
$this
->
forget
();
return
new
Less_Tree_AtRule
(
$name
,
$value
,
$rules
,
$index
,
$isRooted
,
$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
=
[];
$index
=
$this
->
pos
;
do
{
$e
=
$this
->
parseExpression
();
if
(
$e
)
{
$expressions
[]
=
$e
;
if
(
!
$this
->
matchChar
(
','
)
)
{
break
;
}
}
}
while
(
$e
);
if
(
$expressions
)
{
return
new
Less_Tree_Value
(
$expressions
,
$index
);
}
}
private
function
parseImportant
()
{
if
(
$this
->
peekChar
(
'!'
)
&&
$this
->
matchReg
(
'/
\\
G! *important/'
)
)
{
return
' !important'
;
}
}
private
function
parseSub
()
{
$this
->
save
();
if
(
$this
->
matchChar
(
'('
)
)
{
$a
=
$this
->
parseAddition
();
if
(
$a
&&
$this
->
matchChar
(
')'
)
)
{
$this
->
forget
();
$e
=
new
Less_Tree_Expression
(
[
$a
]
);
$e
->
parens
=
true
;
return
$e
;
}
}
$this
->
restore
();
}
/**
* Parses multiplication operation
*
* @return Less_Tree_Operation|null
* @see less-3.13.1.js#parsers.multiplication
*/
private
function
parseMultiplication
()
{
$return
=
$m
=
$this
->
parseOperand
();
if
(
$return
)
{
while
(
true
)
{
$isSpaced
=
$this
->
isWhitespace
(
-
1
);
if
(
$this
->
peekReg
(
'/
\\
G
\/
[*
\/
]/'
)
)
{
break
;
}
$this
->
save
();
$op
=
$this
->
matchChar
(
'/'
)
??
$this
->
matchChar
(
'*'
)
??
$this
->
matchStr
(
'./'
);
if
(
!
$op
)
{
$this
->
forget
();
break
;
}
$a
=
$this
->
parseOperand
();
if
(
!
$a
)
{
$this
->
restore
();
break
;
}
$this
->
forget
();
$m
->
parensInOp
=
true
;
$a
->
parensInOp
=
true
;
$return
=
new
Less_Tree_Operation
(
$op
,
[
$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
)
{
if
(
!
$isSpaced
)
{
$op
=
$this
->
matchChar
(
'+'
)
??
$this
->
matchChar
(
'-'
);
}
if
(
!
$op
)
{
break
;
}
}
$a
=
$this
->
parseMultiplication
();
if
(
!
$a
)
{
break
;
}
$m
->
parensInOp
=
true
;
$a
->
parensInOp
=
true
;
$return
=
new
Less_Tree_Operation
(
$op
,
[
$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
=
new
Less_Tree_Condition
(
'or'
,
$return
,
$b
,
$index
);
}
return
$return
;
}
}
/**
* @see less-2.5.3.js#parsers.condition
*/
private
function
parseCondition
()
{
$index
=
$this
->
pos
;
$negate
=
false
;
$c
=
null
;
if
(
$this
->
matchStr
(
'not'
)
)
{
$negate
=
true
;
}
$this
->
expectChar
(
'('
);
/** @see less-3.13.1.js parsers.atomicCondition */
$a
=
$this
->
parseAddition
()
??
$this
->
parseEntitiesKeyword
()
??
$this
->
parseEntitiesQuoted
()
??
$this
->
parseEntitiesMixinLookup
();
if
(
$a
)
{
$op
=
$this
->
matchReg
(
'/
\\
G(?:>=|<=|=<|[<=>])/'
);
if
(
$op
)
{
/** @see less-3.13.1.js parsers.atomicCondition */
$b
=
$this
->
parseAddition
()
??
$this
->
parseEntitiesKeyword
()
??
$this
->
parseEntitiesQuoted
()
??
$this
->
parseEntitiesMixinLookup
();
if
(
$b
)
{
$c
=
new
Less_Tree_Condition
(
$op
,
$a
,
$b
,
$index
,
$negate
);
}
else
{
$this
->
Error
(
'Unexpected expression'
);
}
}
else
{
$k
=
new
Less_Tree_Keyword
(
'true'
);
$c
=
new
Less_Tree_Condition
(
'='
,
$a
,
$k
,
$index
,
$negate
);
}
$this
->
expectChar
(
')'
);
// @phan-suppress-next-line PhanPossiblyInfiniteRecursionSameParams
return
$this
->
matchStr
(
'and'
)
?
new
Less_Tree_Condition
(
'and'
,
$c
,
$this
->
parseCondition
()
)
:
$c
;
}
}
/**
* An operand is anything that can be part of an operation,
* such as a Color, or a Variable
*
* @see less-3.13.1.js#parsers.operand
*/
private
function
parseOperand
()
{
$negate
=
false
;
$offset
=
$this
->
pos
+
1
;
if
(
$offset
>=
$this
->
input_len
)
{
return
;
}
$char
=
$this
->
input
[
$offset
];
if
(
$char
===
'@'
||
$char
===
'('
||
$char
===
'$'
)
{
$negate
=
$this
->
matchChar
(
'-'
);
}
$o
=
$this
->
parseSub
()
??
$this
->
parseEntitiesDimension
()
??
$this
->
parseEntitiesColor
()
??
$this
->
parseEntitiesVariable
()
??
$this
->
parseEntitiesProperty
()
??
$this
->
parseEntitiesCall
()
??
$this
->
parseEntitiesQuoted
(
true
)
// TODO: from less-3.13.1.js missing entities.colorKeyword()
??
$this
->
parseEntitiesMixinLookup
();
if
(
$negate
)
{
$o
->
parensInOp
=
true
;
$o
=
new
Less_Tree_Negative
(
$o
);
}
return
$o
;
}
/**
* Expressions either represent mathematical operations,
* or white-space delimited Entities.
*
* @return Less_Tree_Expression|null
* @see less-3.13.1.js#parsers.expression
*/
private
function
parseExpression
()
{
$entities
=
[];
$index
=
$this
->
pos
;
do
{
$e
=
$this
->
parseComment
();
if
(
$e
)
{
$entities
[]
=
$e
;
continue
;
}
$e
=
$this
->
parseAddition
()
??
$this
->
parseEntity
();
if
(
$e
instanceof
Less_Tree_Comment
)
{
$e
=
null
;
}
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
[]
=
new
Less_Tree_Anonymous
(
$delim
,
$index
);
}
}
}
}
while
(
$e
);
if
(
$entities
)
{
return
new
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 array<Less_Tree_Keyword|Less_Tree_Variable>
* @see less-3.13.1.js#parsers.ruleProperty
*/
private
function
parseRuleProperty
()
{
$name
=
[];
$index
=
[];
$this
->
save
();
$simpleProperty
=
$this
->
matchReg
(
'/
\\
G([_a-zA-Z0-9-]+)
\s
*:/'
);
if
(
$simpleProperty
)
{
$name
[]
=
new
Less_Tree_Keyword
(
$simpleProperty
[
1
]
);
$this
->
forget
();
return
$name
;
}
$this
->
rulePropertyMatch
(
'/
\\
G(
\*
?)/'
,
$index
,
$name
);
// Consume!
// @phan-suppress-next-line PhanPluginEmptyStatementWhileLoop
while
(
$this
->
rulePropertyMatch
(
'/
\\
G((?:[
\w
-]+)|(?:[@
\$
]
\{
[
\w
-]+
\}
))/'
,
$index
,
$name
)
);
if
(
(
count
(
$name
)
>
1
)
&&
$this
->
rulePropertyMatch
(
'/
\\
G((?:
\+
_|
\+
)?)
\s
*:/'
,
$index
,
$name
)
)
{
$this
->
forget
();
// at last, we have the complete match now. move forward,
// convert name particles to tree objects and return:
if
(
$name
[
0
]
===
''
)
{
array_shift
(
$name
);
array_shift
(
$index
);
}
foreach
(
$name
as
$k
=>
$s
)
{
$firstChar
=
$s
[
0
]
??
''
;
$name
[
$k
]
=
(
$firstChar
!==
'@'
&&
$firstChar
!==
'$'
)
?
new
Less_Tree_Keyword
(
$s
)
:
(
$s
[
0
]
===
'@'
?
new
Less_Tree_Variable
(
'@'
.
substr
(
$s
,
2
,
-
1
),
$index
[
$k
],
$this
->
env
->
currentFileInfo
)
:
new
Less_Tree_Property
(
'$'
.
substr
(
$s
,
2
,
-
1
),
$index
[
$k
],
$this
->
env
->
currentFileInfo
)
);
}
return
$name
;
}
else
{
$this
->
restore
();
}
}
private
function
rulePropertyMatch
(
$re
,
&
$index
,
&
$name
)
{
$i
=
$this
->
pos
;
$chunk
=
$this
->
matchReg
(
$re
);
if
(
$chunk
)
{
$index
[]
=
$i
;
$name
[]
=
$chunk
[
1
];
return
true
;
}
}
public
static
function
serializeVars
(
$vars
)
{
$s
=
''
;
foreach
(
$vars
as
$name
=>
$value
)
{
if
(
strval
(
$value
)
===
""
)
{
$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
*
* @internal For internal use only
* @param mixed $a
* @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
*
* @internal For internal use only
*/
public
static
function
round
(
$input
,
$precision
=
0
)
{
$precision
=
pow
(
10
,
$precision
);
$i
=
$input
*
$precision
;
$ceil
=
ceil
(
$i
);
$floor
=
floor
(
$i
);
if
(
(
$ceil
-
$i
)
<=
(
$i
-
$floor
)
)
{
return
$ceil
/
$precision
;
}
else
{
return
$floor
/
$precision
;
}
}
/** @return never */
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?:)?
\/\/
/i'
,
$path
)
)
{
return
$winPath
?
''
:
false
;
}
else
{
$path
=
realpath
(
$path
);
if
(
$winPath
)
{
$path
=
self
::
WinPath
(
$path
);
}
return
$path
;
}
}
public
function
CacheEnabled
()
{
return
(
self
::
$options
[
'cache_method'
]
&&
(
Less_Cache
::
$cache_dir
||
(
self
::
$options
[
'cache_method'
]
==
'callback'
)
)
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sat, May 16, 17:58 (9 h, 5 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
a9/25/3d30749db6f61f266ffca6f34e38
Default Alt Text
Parser.php (82 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment