massive update

This commit is contained in:
Michael Becker 2024-01-02 00:17:40 -05:00
parent 03947054f5
commit d828f2ec46
79 changed files with 11500 additions and 149 deletions

View File

@ -3,28 +3,85 @@ function AdditionalDetailWidget(parent)
this.Parent = parent;
this.Show = function ()
{
System.ClassList.Add(this.Parent, "Visible");
System.ClassList.Add(this.Parent, "apb-selected");
};
this.Hide = function ()
{
System.ClassList.Remove(this.Parent, "Visible");
System.ClassList.Remove(this.Parent, "apb-selected");
};
this.Toggle = function ()
{
System.ClassList.Toggle(this.Parent, "apb-selected");
};
this.TextLink = parent.childNodes[0];
this.ButtonLink = parent.childNodes[1];
// aria
this.TextLink.NativeObject = this;
this.TextLink.addEventListener("keydown", function(e)
{
console.log(e);
if (e.keyCode == 9)
{
if (!e.shiftKey)
{
this.focusIndex = 1;
// tab
this.NativeObject.ButtonLink.tabIndex = -1;
this.NativeObject.ButtonLink.focus();
console.log("focused");
console.log(this.NativeObject.ButtonLink);
// just.. eat it!
e.preventDefault();
e.stopPropagation();
return false;
}
}
});
this.TextLink.NativeObject = this;
this.TextLink.addEventListener("focus", function(e)
{
if (e.relatedTarget !== null)
{
var pos = this.compareDocumentPosition(e.relatedTarget);
if (pos == Node.DOCUMENT_POSITION_FOLLOWING && e.relatedTarget !== this.NativeObject.ButtonLink)
{
this.NativeObject.ButtonLink.focus();
}
}
});
this.ButtonLink.NativeObject = this;
this.ButtonLink.addEventListener("click", function (e)
{
if (e.which == MouseButtons.Primary)
{
this.NativeObject.Show();
e.preventDefault();
e.stopPropagation();
return false;
}
});
this.ButtonLink.addEventListener("keydown", function (e)
{
if (e.keyCode == 13 || e.keyCode == 32) // enter or space
{
this.NativeObject.Toggle();
e.preventDefault();
e.stopPropagation();
return false;
}
// console.log("keydown on button"); console.log(e);
});
}
window.addEventListener("load", function(e)
{
var items = document.getElementsByClassName("AdditionalDetailWidget");
var items = document.getElementsByClassName("uwt-actionpreviewbutton");
for (var i = 0; i < items.length; i++)
{
items[i].NativeObject = new AdditionalDetailWidget(items[i]);
@ -42,9 +99,21 @@ window.addEventListener("mousedown", function (e)
{
sender = e.srcElement;
}
if (!System.TerminateIfSenderIs(sender, ["AdditionalDetailWidget"]))
if (!System.TerminateIfSenderIs(sender, ["uwt-actionpreviewbutton"]))
{
var items = document.getElementsByClassName("AdditionalDetailWidget");
var items = document.getElementsByClassName("uwt-actionpreviewbutton");
for (var i = 0; i < items.length; i++)
{
items[i].NativeObject.Hide();
}
}
});
window.addEventListener("keydown", function (e)
{
if (e.keyCode == 27)
{
// cancel all APBs if they are open
var items = document.getElementsByClassName("uwt-actionpreviewbutton");
for (var i = 0; i < items.length; i++)
{
items[i].NativeObject.Hide();

View File

@ -2,6 +2,23 @@ function Button(parentElement)
{
this.ParentElement = parentElement;
if (this.ParentElement.tagName == "A")
{
console.log(parentElement);
this.ParentElement.addEventListener("click", function(e)
{
if (System.ClassList.Contains(this, "uwt-toggle"))
{
System.ClassList.Toggle(this, "uwt-selected");
e.preventDefault();
e.stopPropagation();
return false;
}
});
}
return;
this.ButtonElement = this.ParentElement.children[0];
this.ButtonElement.NativeObject = this;
@ -59,10 +76,10 @@ function Button(parentElement)
}
window.addEventListener("load", function(e)
{
var items = document.getElementsByClassName("pwt-Button");
var items = document.getElementsByClassName("uwt-button");
for (var i = 0; i < items.length; i++)
{
if (items[i].tagName.toLowerCase() != "div") continue;
// if (items[i].tagName.toLowerCase() != "div") continue;
items[i].NativeObject = new Button(items[i]);
}
});

View File

@ -71,6 +71,22 @@ function ListViewItem(parentListView, index)
return this.mvarParentListView;
};
this.mvarParentElement = parentListView.ItemsElement.children[index];
this.mvarParentElement.addEventListener("mousedown", function(e)
{
if (e.buttons == 1 || !System.ClassList.Contains(this, "uwt-selected"))
{
if (!e.ctrlKey)
{
for (var i = 0; i < this.parentElement.children.length; i++)
{
System.ClassList.Remove(this.parentElement.children[i], "uwt-selected");
}
}
System.ClassList.Add(this, "uwt-selected");
}
});
this.mvarIndex = index;
this.get_Index = function()
{
@ -95,10 +111,18 @@ function ListViewColumn()
function ListView(parentElement)
{
this.ParentElement = parentElement;
this.ColumnHeaderElement = this.ParentElement.children[0];
//this.EmptyMessageElement = this.ParentElement.children[1];
this.ItemsElement = this.ParentElement.children[1];
this.ContentElement = this.ParentElement.children[1];
this.ColumnHeaderElement = this.ContentElement.children[0];
//this.EmptyMessageElement = this.ContentElement.children[1];
this.ItemsElement = this.ContentElement.children[1];
for (var i = 0; i < this.ItemsElement.children.length; i++)
{
this.ItemsElement.children[i].NativeObject = new ListViewItem(this, i);
}
/*
this.AddRemoveColumnHeaderElement = this.ColumnHeaderElement.children[0];
this.AddRemoveColumnHeaderAddColumnButton = this.AddRemoveColumnHeaderElement.children[0];
@ -107,6 +131,7 @@ function ListView(parentElement)
{
this.NativeObject.InsertRowAfter(-1);
});
*/
/**
* Inserts a row after the row at the specified index.
@ -180,7 +205,7 @@ function ListView(parentElement)
var row = this.ItemsElement.children[i];
row.m_Index = i;
/*
var AddRemoveRowItemColumnElement = row.children[0];
var AddRowButton = AddRemoveRowItemColumnElement.children[0];
if (AddRowButton)
@ -193,12 +218,14 @@ function ListView(parentElement)
{
RemoveRowButton.m_Index = i;
}
*/
// if it has already been processed, skip it
if (row.NativeObject) continue;
row.NativeObject = this;
/*
if (AddRowButton)
{
AddRowButton.NativeObject = this;
@ -215,6 +242,7 @@ function ListView(parentElement)
this.NativeObject.Rows.RemoveAt(this.m_Index);
});
}
*/
row.addEventListener("mousedown", function(e)
{
@ -482,7 +510,7 @@ function ListView(parentElement)
}
window.addEventListener("load", function(e)
{
var items = document.getElementsByClassName("ListView");
var items = document.getElementsByClassName("uwt-listview");
for (var i = 0; i < items.length; i++)
{
items[i].NativeObject = new ListView(items[i]);

View File

@ -45,7 +45,7 @@ window.addEventListener("load", function(e)
for (var i = 0; i < items.length; i++)
{
if (items[i].NativeObject) continue;
items[i].NativeObject = new uwt-menu(items[i]);
items[i].NativeObject = new Menu(items[i]);
}
});

View File

@ -1060,6 +1060,18 @@
return array($color, $delta);
}
/* BEGIN: added by MB 2023-12-24 09:23 */
protected function lib_tint($args) {
list($color, $delta) = $args[2];
return $this->lib_mix(array('list', ' ', array(array('color', 255, 255, 255), $color, $delta)));
}
protected function lib_shade($args) {
list($color, $delta) = $args[2];
return $this->lib_mix(array('list', ' ', array(array('color', 0, 0, 0), $color, $delta)));
}
/* END: added by MB 2023-12-24 09:23 */
protected function lib_darken($args) {
list($color, $delta) = $this->colorArgs($args);
@ -2209,7 +2221,8 @@
// responsible for taking a string of LESS code and converting it into a
// syntax tree
class LessStyleSheetParser {
class LessStyleSheetParser
{
static protected $nextBlockId = 0; // used to uniquely identify blocks
static protected $precedence = array(

View File

@ -0,0 +1,2 @@
.easymin
Autoloader.php

View File

@ -0,0 +1,57 @@
<?php
/**
* Autoloader
*/
class Less_Autoloader {
/** @var bool */
protected static $registered = false;
/**
* Register the autoloader in the SPL autoloader
*
* @return void
* @throws Exception If there was an error in registration
*/
public static function register() {
if ( self::$registered ) {
return;
}
if ( !spl_autoload_register( [ __CLASS__, 'loadClass' ] ) ) {
throw new Exception( 'Unable to register Less_Autoloader::loadClass as an autoloading method.' );
}
self::$registered = true;
}
/**
* Unregister the autoloader
*
* @return void
*/
public static function unregister() {
spl_autoload_unregister( [ __CLASS__, 'loadClass' ] );
self::$registered = false;
}
/**
* Load the class
*
* @param string $className The class to load
*/
public static function loadClass( $className ) {
// handle only package classes
if ( strpos( $className, 'Less_' ) !== 0 ) {
return;
}
$className = substr( $className, 5 );
$fileName = __DIR__ . DIRECTORY_SEPARATOR . str_replace( '_', DIRECTORY_SEPARATOR, $className ) . '.php';
require $fileName;
return true;
}
}

View File

@ -0,0 +1,279 @@
<?php
/**
* Utility for handling the generation and caching of css files
*/
class Less_Cache {
/** @var string|false Directory less.php can use for storing data */
public static $cache_dir = false;
/** @var string Prefix for the storing data */
public static $prefix = 'lessphp_';
/** @var string Prefix for the storing vars */
public static $prefix_vars = 'lessphpvars_';
/**
* @var int Specifies the number of seconds after which data created by less.php will be seen
* as 'garbage' and potentially cleaned up
*/
public static $gc_lifetime = 604800;
/**
* Save and reuse the results of compiled less files.
* The first call to Get() will generate css and save it.
* Subsequent calls to Get() with the same arguments will return the same css filename
*
* @param array $less_files Array of .less files to compile
* @param array $parser_options Array of compiler options
* @param array $modify_vars Array of variables
* @return string|false Name of the css file
*/
public static function Get( $less_files, $parser_options = [], $modify_vars = [] ) {
// check $cache_dir
if ( isset( $parser_options['cache_dir'] ) ) {
self::$cache_dir = $parser_options['cache_dir'];
}
if ( empty( self::$cache_dir ) ) {
throw new Exception( 'cache_dir not set' );
}
if ( isset( $parser_options['prefix'] ) ) {
self::$prefix = $parser_options['prefix'];
}
if ( empty( self::$prefix ) ) {
throw new Exception( 'prefix not set' );
}
if ( isset( $parser_options['prefix_vars'] ) ) {
self::$prefix_vars = $parser_options['prefix_vars'];
}
if ( empty( self::$prefix_vars ) ) {
throw new Exception( 'prefix_vars not set' );
}
self::CheckCacheDir();
$less_files = (array)$less_files;
// create a file for variables
if ( !empty( $modify_vars ) ) {
$lessvars = Less_Parser::serializeVars( $modify_vars );
$vars_file = self::$cache_dir . self::$prefix_vars . sha1( $lessvars ) . '.less';
if ( !file_exists( $vars_file ) ) {
file_put_contents( $vars_file, $lessvars );
}
$less_files += [ $vars_file => '/' ];
}
// generate name for compiled css file
$hash = md5( json_encode( $less_files ) );
$list_file = self::$cache_dir . self::$prefix . $hash . '.list';
// check cached content
if ( !isset( $parser_options['use_cache'] ) || $parser_options['use_cache'] === true ) {
if ( file_exists( $list_file ) ) {
self::ListFiles( $list_file, $list, $cached_name );
$compiled_name = self::CompiledName( $list, $hash );
// if $cached_name is the same as the $compiled name, don't regenerate
if ( !$cached_name || $cached_name === $compiled_name ) {
$output_file = self::OutputFile( $compiled_name, $parser_options );
if ( $output_file && file_exists( $output_file ) ) {
@touch( $list_file );
return basename( $output_file ); // for backwards compatibility, we just return the name of the file
}
}
}
}
$compiled = self::Cache( $less_files, $parser_options );
if ( !$compiled ) {
return false;
}
$compiled_name = self::CompiledName( $less_files, $hash );
$output_file = self::OutputFile( $compiled_name, $parser_options );
// save the file list
$list = $less_files;
$list[] = $compiled_name;
$cache = implode( "\n", $list );
file_put_contents( $list_file, $cache );
// save the css
file_put_contents( $output_file, $compiled );
// clean up
self::CleanCache();
return basename( $output_file );
}
/**
* Force the compiler to regenerate the cached css file
*
* @param array $less_files Array of .less files to compile
* @param array $parser_options Array of compiler options
* @param array $modify_vars Array of variables
* @return string Name of the css file
*/
public static function Regen( $less_files, $parser_options = [], $modify_vars = [] ) {
$parser_options['use_cache'] = false;
return self::Get( $less_files, $parser_options, $modify_vars );
}
public static function Cache( &$less_files, $parser_options = [] ) {
$parser_options['cache_dir'] = self::$cache_dir;
$parser = new Less_Parser( $parser_options );
// combine files
foreach ( $less_files as $file_path => $uri_or_less ) {
// treat as less markup if there are newline characters
if ( strpos( $uri_or_less, "\n" ) !== false ) {
$parser->Parse( $uri_or_less );
continue;
}
$parser->ParseFile( $file_path, $uri_or_less );
}
$compiled = $parser->getCss();
$less_files = $parser->AllParsedFiles();
return $compiled;
}
private static function OutputFile( $compiled_name, $parser_options ) {
// custom output file
if ( !empty( $parser_options['output'] ) ) {
// relative to cache directory?
if ( preg_match( '#[\\\\/]#', $parser_options['output'] ) ) {
return $parser_options['output'];
}
return self::$cache_dir . $parser_options['output'];
}
return self::$cache_dir . $compiled_name;
}
private static function CompiledName( $files, $extrahash ) {
// save the file list
$temp = [ Less_Version::cache_version ];
foreach ( $files as $file ) {
$temp[] = filemtime( $file ) . "\t" . filesize( $file ) . "\t" . $file;
}
return self::$prefix . sha1( json_encode( $temp ) . $extrahash ) . '.css';
}
public static function SetCacheDir( $dir ) {
self::$cache_dir = $dir;
self::CheckCacheDir();
}
public static function CheckCacheDir() {
self::$cache_dir = str_replace( '\\', '/', self::$cache_dir );
self::$cache_dir = rtrim( self::$cache_dir, '/' ) . '/';
if ( !file_exists( self::$cache_dir ) ) {
if ( !mkdir( self::$cache_dir ) ) {
throw new Less_Exception_Parser( 'Less.php cache directory couldn\'t be created: ' . self::$cache_dir );
}
} elseif ( !is_dir( self::$cache_dir ) ) {
throw new Less_Exception_Parser( 'Less.php cache directory doesn\'t exist: ' . self::$cache_dir );
} elseif ( !is_writable( self::$cache_dir ) ) {
throw new Less_Exception_Parser( 'Less.php cache directory isn\'t writable: ' . self::$cache_dir );
}
}
/**
* Delete unused less.php files
*/
public static function CleanCache() {
static $clean = false;
if ( $clean || empty( self::$cache_dir ) ) {
return;
}
$clean = true;
// only remove files with extensions created by less.php
// css files removed based on the list files
$remove_types = [ 'lesscache' => 1,'list' => 1,'less' => 1,'map' => 1 ];
$files = scandir( self::$cache_dir );
if ( !$files ) {
return;
}
$check_time = time() - self::$gc_lifetime;
foreach ( $files as $file ) {
// don't delete if the file wasn't created with less.php
if ( strpos( $file, self::$prefix ) !== 0 ) {
continue;
}
$parts = explode( '.', $file );
$type = array_pop( $parts );
if ( !isset( $remove_types[$type] ) ) {
continue;
}
$full_path = self::$cache_dir . $file;
$mtime = filemtime( $full_path );
// don't delete if it's a relatively new file
if ( $mtime > $check_time ) {
continue;
}
// delete the list file and associated css file
if ( $type === 'list' ) {
self::ListFiles( $full_path, $list, $css_file_name );
if ( $css_file_name ) {
$css_file = self::$cache_dir . $css_file_name;
if ( file_exists( $css_file ) ) {
unlink( $css_file );
}
}
}
unlink( $full_path );
}
}
/**
* Get the list of less files and generated css file from a list file
*/
public static function ListFiles( $list_file, &$list, &$css_file_name ) {
$list = explode( "\n", file_get_contents( $list_file ) );
// pop the cached name that should match $compiled_name
$css_file_name = array_pop( $list );
if ( !preg_match( '/^' . self::$prefix . '[a-f0-9]+\.css$/', $css_file_name ) ) {
$list[] = $css_file_name;
$css_file_name = false;
}
}
}

View File

@ -0,0 +1,176 @@
<?php
/**
* Utility for css colors
*
* @private
*/
class Less_Colors {
private const COLORS = [
'aliceblue' => '#f0f8ff',
'antiquewhite' => '#faebd7',
'aqua' => '#00ffff',
'aquamarine' => '#7fffd4',
'azure' => '#f0ffff',
'beige' => '#f5f5dc',
'bisque' => '#ffe4c4',
'black' => '#000000',
'blanchedalmond' => '#ffebcd',
'blue' => '#0000ff',
'blueviolet' => '#8a2be2',
'brown' => '#a52a2a',
'burlywood' => '#deb887',
'cadetblue' => '#5f9ea0',
'chartreuse' => '#7fff00',
'chocolate' => '#d2691e',
'coral' => '#ff7f50',
'cornflowerblue' => '#6495ed',
'cornsilk' => '#fff8dc',
'crimson' => '#dc143c',
'cyan' => '#00ffff',
'darkblue' => '#00008b',
'darkcyan' => '#008b8b',
'darkgoldenrod' => '#b8860b',
'darkgray' => '#a9a9a9',
'darkgrey' => '#a9a9a9',
'darkgreen' => '#006400',
'darkkhaki' => '#bdb76b',
'darkmagenta' => '#8b008b',
'darkolivegreen' => '#556b2f',
'darkorange' => '#ff8c00',
'darkorchid' => '#9932cc',
'darkred' => '#8b0000',
'darksalmon' => '#e9967a',
'darkseagreen' => '#8fbc8f',
'darkslateblue' => '#483d8b',
'darkslategray' => '#2f4f4f',
'darkslategrey' => '#2f4f4f',
'darkturquoise' => '#00ced1',
'darkviolet' => '#9400d3',
'deeppink' => '#ff1493',
'deepskyblue' => '#00bfff',
'dimgray' => '#696969',
'dimgrey' => '#696969',
'dodgerblue' => '#1e90ff',
'firebrick' => '#b22222',
'floralwhite' => '#fffaf0',
'forestgreen' => '#228b22',
'fuchsia' => '#ff00ff',
'gainsboro' => '#dcdcdc',
'ghostwhite' => '#f8f8ff',
'gold' => '#ffd700',
'goldenrod' => '#daa520',
'gray' => '#808080',
'grey' => '#808080',
'green' => '#008000',
'greenyellow' => '#adff2f',
'honeydew' => '#f0fff0',
'hotpink' => '#ff69b4',
'indianred' => '#cd5c5c',
'indigo' => '#4b0082',
'ivory' => '#fffff0',
'khaki' => '#f0e68c',
'lavender' => '#e6e6fa',
'lavenderblush' => '#fff0f5',
'lawngreen' => '#7cfc00',
'lemonchiffon' => '#fffacd',
'lightblue' => '#add8e6',
'lightcoral' => '#f08080',
'lightcyan' => '#e0ffff',
'lightgoldenrodyellow' => '#fafad2',
'lightgray' => '#d3d3d3',
'lightgrey' => '#d3d3d3',
'lightgreen' => '#90ee90',
'lightpink' => '#ffb6c1',
'lightsalmon' => '#ffa07a',
'lightseagreen' => '#20b2aa',
'lightskyblue' => '#87cefa',
'lightslategray' => '#778899',
'lightslategrey' => '#778899',
'lightsteelblue' => '#b0c4de',
'lightyellow' => '#ffffe0',
'lime' => '#00ff00',
'limegreen' => '#32cd32',
'linen' => '#faf0e6',
'magenta' => '#ff00ff',
'maroon' => '#800000',
'mediumaquamarine' => '#66cdaa',
'mediumblue' => '#0000cd',
'mediumorchid' => '#ba55d3',
'mediumpurple' => '#9370d8',
'mediumseagreen' => '#3cb371',
'mediumslateblue' => '#7b68ee',
'mediumspringgreen' => '#00fa9a',
'mediumturquoise' => '#48d1cc',
'mediumvioletred' => '#c71585',
'midnightblue' => '#191970',
'mintcream' => '#f5fffa',
'mistyrose' => '#ffe4e1',
'moccasin' => '#ffe4b5',
'navajowhite' => '#ffdead',
'navy' => '#000080',
'oldlace' => '#fdf5e6',
'olive' => '#808000',
'olivedrab' => '#6b8e23',
'orange' => '#ffa500',
'orangered' => '#ff4500',
'orchid' => '#da70d6',
'palegoldenrod' => '#eee8aa',
'palegreen' => '#98fb98',
'paleturquoise' => '#afeeee',
'palevioletred' => '#d87093',
'papayawhip' => '#ffefd5',
'peachpuff' => '#ffdab9',
'peru' => '#cd853f',
'pink' => '#ffc0cb',
'plum' => '#dda0dd',
'powderblue' => '#b0e0e6',
'purple' => '#800080',
'red' => '#ff0000',
'rosybrown' => '#bc8f8f',
'royalblue' => '#4169e1',
'saddlebrown' => '#8b4513',
'salmon' => '#fa8072',
'sandybrown' => '#f4a460',
'seagreen' => '#2e8b57',
'seashell' => '#fff5ee',
'sienna' => '#a0522d',
'silver' => '#c0c0c0',
'skyblue' => '#87ceeb',
'slateblue' => '#6a5acd',
'slategray' => '#708090',
'slategrey' => '#708090',
'snow' => '#fffafa',
'springgreen' => '#00ff7f',
'steelblue' => '#4682b4',
'tan' => '#d2b48c',
'teal' => '#008080',
'thistle' => '#d8bfd8',
'tomato' => '#ff6347',
'turquoise' => '#40e0d0',
'violet' => '#ee82ee',
'wheat' => '#f5deb3',
'white' => '#ffffff',
'whitesmoke' => '#f5f5f5',
'yellow' => '#ffff00',
'yellowgreen' => '#9acd32',
];
/**
* @param string $color
* @return bool
*/
public static function hasOwnProperty( string $color ): bool {
return isset( self::COLORS[$color] );
}
/**
* @param string $color Should be an existing color name,
* checked via hasOwnProperty()
* @return string the corresponding hexadecimal representation
*/
public static function color( string $color ): string {
return self::COLORS[$color];
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* @private
*/
abstract class Less_Configurable {
/**
* Array of options
*
* @var array
*/
protected $options = [];
/**
* Array of default options
*
* @var array
*/
protected $defaultOptions = [];
/**
* @param array $options
*/
public function setOptions( $options ) {
$options = array_intersect_key( $options, $this->defaultOptions );
$this->options = array_merge( $this->defaultOptions, $this->options, $options );
}
/**
* Get an option value by name
*
* If the option is empty or not set a NULL value will be returned.
*
* @param string $name
* @param mixed $default Default value if confiuration of $name is not present
* @return mixed
*/
public function getOption( $name, $default = null ) {
if ( isset( $this->options[$name] ) ) {
return $this->options[$name];
}
return $default;
}
/**
* Set an option
*
* @param string $name
* @param mixed $value
*/
public function setOption( $name, $value ) {
$this->options[$name] = $value;
}
}

View File

@ -0,0 +1,162 @@
<?php
/**
* @private
*/
class Less_Environment {
/**
* Information about the current file - for error reporting and importing and making urls relative etc.
*
* - rootpath: rootpath to append to URLs
*
* @var array|null
*/
public $currentFileInfo;
/** @var bool Whether we are currently importing multiple copies */
public $importMultiple = false;
/**
* @var array
*/
public $frames = [];
/** @var Less_Tree_Media[] */
public $mediaBlocks = [];
/** @var Less_Tree_Media[] */
public $mediaPath = [];
public static $parensStack = 0;
public static $tabLevel = 0;
public static $lastRule = false;
public static $_outputMap;
public static $mixin_stack = 0;
public static $mathOn = true;
/**
* @var array
*/
public $functions = [];
public function Init() {
self::$parensStack = 0;
self::$tabLevel = 0;
self::$lastRule = false;
self::$mixin_stack = 0;
if ( Less_Parser::$options['compress'] ) {
self::$_outputMap = [
',' => ',',
': ' => ':',
'' => '',
' ' => ' ',
':' => ' :',
'+' => '+',
'~' => '~',
'>' => '>',
'|' => '|',
'^' => '^',
'^^' => '^^'
];
} else {
self::$_outputMap = [
',' => ', ',
': ' => ': ',
'' => '',
' ' => ' ',
':' => ' :',
'+' => ' + ',
'~' => ' ~ ',
'>' => ' > ',
'|' => '|',
'^' => ' ^ ',
'^^' => ' ^^ '
];
}
}
public function copyEvalEnv( $frames = [] ) {
$new_env = new self();
$new_env->frames = $frames;
return $new_env;
}
/**
* @return bool
* @see Eval.prototype.isMathOn in less.js 3.0.0 https://github.com/less/less.js/blob/v3.0.0/dist/less.js#L1007
*/
public static function isMathOn() {
if ( !self::$mathOn ) {
return false;
}
return !Less_Parser::$options['strictMath'] || self::$parensStack;
}
/**
* @param string $path
* @return bool
* @see less-2.5.3.js#Eval.isPathRelative
*/
public static function isPathRelative( $path ) {
return !preg_match( '/^(?:[a-z-]+:|\/|#)/', $path );
}
/**
* Canonicalize a path by resolving references to '/./', '/../'
* Does not remove leading "../"
* @param string $path or url
* @return string Canonicalized path
*/
public static function normalizePath( $path ) {
$segments = explode( '/', $path );
$segments = array_reverse( $segments );
$path = [];
$path_len = 0;
while ( $segments ) {
$segment = array_pop( $segments );
switch ( $segment ) {
case '.':
break;
case '..':
// @phan-suppress-next-line PhanTypeInvalidDimOffset False positive
if ( !$path_len || ( $path[$path_len - 1] === '..' ) ) {
$path[] = $segment;
$path_len++;
} else {
array_pop( $path );
$path_len--;
}
break;
default:
$path[] = $segment;
$path_len++;
break;
}
}
return implode( '/', $path );
}
public function unshiftFrame( $frame ) {
array_unshift( $this->frames, $frame );
}
public function shiftFrame() {
return array_shift( $this->frames );
}
}

View File

@ -0,0 +1,208 @@
<?php
/**
* @private
*/
class Less_Exception_Chunk extends Less_Exception_Parser {
protected $parserCurrentIndex = 0;
protected $emitFrom = 0;
protected $input_len;
/**
* @param string $input
* @param Exception|null $previous Previous exception
* @param int|null $index The current parser index
* @param array|null $currentFile The file
* @param int $code The exception code
*/
public function __construct( $input, Exception $previous = null, $index = null, $currentFile = null, $code = 0 ) {
$this->message = 'ParseError: Unexpected input'; // default message
$this->index = $index;
$this->currentFile = $currentFile;
$this->input = $input;
$this->input_len = strlen( $input );
$this->Chunks();
$this->genMessage();
}
/**
* See less.js chunks()
* We don't actually need the chunks
*/
protected function Chunks() {
$level = 0;
$parenLevel = 0;
$lastMultiCommentEndBrace = null;
$lastOpening = null;
$lastMultiComment = null;
$lastParen = null;
// phpcs:ignore Generic.CodeAnalysis.JumbledIncrementer
for ( $this->parserCurrentIndex = 0; $this->parserCurrentIndex < $this->input_len; $this->parserCurrentIndex++ ) {
$cc = $this->CharCode( $this->parserCurrentIndex );
if ( ( ( $cc >= 97 ) && ( $cc <= 122 ) ) || ( $cc < 34 ) ) {
// a-z or whitespace
continue;
}
switch ( $cc ) {
// (
case 40:
$parenLevel++;
$lastParen = $this->parserCurrentIndex;
break;
// )
case 41:
$parenLevel--;
if ( $parenLevel < 0 ) {
return $this->fail( "missing opening `(`" );
}
break;
// ;
case 59:
// if (!$parenLevel) { $this->emitChunk(); }
break;
// {
case 123:
$level++;
$lastOpening = $this->parserCurrentIndex;
break;
// }
case 125:
$level--;
if ( $level < 0 ) {
return $this->fail( "missing opening `{`" );
}
// if (!$level && !$parenLevel) { $this->emitChunk(); }
break;
// \
case 92:
if ( $this->parserCurrentIndex < $this->input_len - 1 ) {
$this->parserCurrentIndex++;
break;
}
return $this->fail( "unescaped `\\`" );
// ", ' and `
case 34:
case 39:
case 96:
$matched = 0;
$currentChunkStartIndex = $this->parserCurrentIndex;
for ( $this->parserCurrentIndex += 1; $this->parserCurrentIndex < $this->input_len; $this->parserCurrentIndex++ ) {
$cc2 = $this->CharCode( $this->parserCurrentIndex );
if ( $cc2 > 96 ) {
continue;
}
if ( $cc2 == $cc ) {
$matched = 1;
break;
}
if ( $cc2 == 92 ) { // \
if ( $this->parserCurrentIndex == $this->input_len - 1 ) {
return $this->fail( "unescaped `\\`" );
}
$this->parserCurrentIndex++;
}
}
if ( $matched ) {
break;
}
return $this->fail( "unmatched `" . chr( $cc ) . "`", $currentChunkStartIndex );
// /, check for comment
case 47:
if ( $parenLevel || ( $this->parserCurrentIndex == $this->input_len - 1 ) ) {
break;
}
$cc2 = $this->CharCode( $this->parserCurrentIndex + 1 );
if ( $cc2 == 47 ) {
// //, find lnfeed
for ( $this->parserCurrentIndex += 2; $this->parserCurrentIndex < $this->input_len; $this->parserCurrentIndex++ ) {
$cc2 = $this->CharCode( $this->parserCurrentIndex );
if ( ( $cc2 <= 13 ) && ( ( $cc2 == 10 ) || ( $cc2 == 13 ) ) ) {
break;
}
}
} elseif ( $cc2 == 42 ) {
// /*, find */
$lastMultiComment = $currentChunkStartIndex = $this->parserCurrentIndex;
for ( $this->parserCurrentIndex += 2; $this->parserCurrentIndex < $this->input_len - 1; $this->parserCurrentIndex++ ) {
$cc2 = $this->CharCode( $this->parserCurrentIndex );
if ( $cc2 == 125 ) {
$lastMultiCommentEndBrace = $this->parserCurrentIndex;
}
if ( $cc2 != 42 ) {
continue;
}
if ( $this->CharCode( $this->parserCurrentIndex + 1 ) == 47 ) {
break;
}
}
if ( $this->parserCurrentIndex == $this->input_len - 1 ) {
return $this->fail( "missing closing `*/`", $currentChunkStartIndex );
}
}
break;
// *, check for unmatched */
case 42:
if ( ( $this->parserCurrentIndex < $this->input_len - 1 ) && ( $this->CharCode( $this->parserCurrentIndex + 1 ) == 47 ) ) {
return $this->fail( "unmatched `/*`" );
}
break;
}
}
if ( $level !== 0 ) {
if ( ( $lastMultiComment > $lastOpening ) && ( $lastMultiCommentEndBrace > $lastMultiComment ) ) {
return $this->fail( "missing closing `}` or `*/`", $lastOpening );
} else {
return $this->fail( "missing closing `}`", $lastOpening );
}
} elseif ( $parenLevel !== 0 ) {
return $this->fail( "missing closing `)`", $lastParen );
}
// chunk didn't fail
//$this->emitChunk(true);
}
public function CharCode( $pos ) {
return ord( $this->input[$pos] );
}
public function fail( $msg, $index = null ) {
if ( !$index ) {
$this->index = $this->parserCurrentIndex;
} else {
$this->index = $index;
}
$this->message = 'ParseError: ' . $msg;
}
/*
function emitChunk( $force = false ){
$len = $this->parserCurrentIndex - $this->emitFrom;
if ((($len < 512) && !$force) || !$len) {
return;
}
$chunks[] = substr($this->input, $this->emitFrom, $this->parserCurrentIndex + 1 - $this->emitFrom );
$this->emitFrom = $this->parserCurrentIndex + 1;
}
*/
}

View File

@ -0,0 +1,8 @@
<?php
/**
* Compiler Exception
*/
class Less_Exception_Compiler extends Less_Exception_Parser {
}

View File

@ -0,0 +1,103 @@
<?php
/**
* Parser Exception
*/
class Less_Exception_Parser extends Exception {
/**
* The current file
*
* @var array
*/
public $currentFile;
/**
* The current parser index
*
* @var int
*/
public $index;
protected $input;
protected $details = [];
/**
* @param string|null $message
* @param Exception|null $previous Previous exception
* @param int|null $index The current parser index
* @param array|null $currentFile The file
* @param int $code The exception code
*/
public function __construct( $message = null, Exception $previous = null, $index = null, $currentFile = null, $code = 0 ) {
parent::__construct( $message, $code, $previous );
$this->currentFile = $currentFile;
$this->index = $index;
$this->genMessage();
}
protected function getInput() {
if ( !$this->input && $this->currentFile && $this->currentFile['filename'] && file_exists( $this->currentFile['filename'] ) ) {
$this->input = file_get_contents( $this->currentFile['filename'] );
}
}
/**
* Set a message based on the exception info
*/
public function genMessage() {
if ( $this->currentFile && $this->currentFile['filename'] ) {
$this->message .= ' in ' . basename( $this->currentFile['filename'] );
}
if ( $this->index !== null ) {
$this->getInput();
if ( $this->input ) {
$line = self::getLineNumber();
$this->message .= ' on line ' . $line . ', column ' . self::getColumn();
$lines = explode( "\n", $this->input );
$count = count( $lines );
$start_line = max( 0, $line - 3 );
$last_line = min( $count, $start_line + 6 );
$num_len = strlen( $last_line );
for ( $i = $start_line; $i < $last_line; $i++ ) {
$this->message .= "\n" . str_pad( (string)( $i + 1 ), $num_len, '0', STR_PAD_LEFT ) . '| ' . $lines[$i];
}
}
}
}
/**
* Returns the line number the error was encountered
*
* @return int
*/
public function getLineNumber() {
if ( $this->index ) {
// https://bugs.php.net/bug.php?id=49790
if ( ini_get( "mbstring.func_overload" ) ) {
return substr_count( substr( $this->input, 0, $this->index ), "\n" ) + 1;
} else {
return substr_count( $this->input, "\n", 0, $this->index ) + 1;
}
}
return 1;
}
/**
* Returns the column the error was encountered
*
* @return int
*/
public function getColumn() {
$part = substr( $this->input, 0, $this->index );
$pos = strrpos( $part, "\n" );
return $this->index - $pos;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
./Parser.php
./Colors.php
./Environment.php
./Functions.php
./Mime.php
./Tree.php
./Output.php
./Visitor.php
./VisitorReplacing.php
./Configurable.php
./Tree
./Visitor
./Exception/Parser.php
./Exception/
./Output
./SourceMap

View File

@ -0,0 +1,39 @@
<?php
/**
* Mime lookup
*
* @private
*/
class Less_Mime {
/**
* this map is intentionally incomplete
* if you want more, install 'mime' dep
* @var array<string,string>
*/
private static $types = [
'.htm' => 'text/html',
'.html' => 'text/html',
'.gif' => 'image/gif',
'.jpg' => 'image/jpeg',
'.jpeg' => 'image/jpeg',
'.png' => 'image/png',
'.ttf' => 'application/x-font-ttf',
'.otf' => 'application/x-font-otf',
'.eot' => 'application/vnd.ms-fontobject',
'.woff' => 'application/x-font-woff',
'.svg' => 'image/svg+xml',
];
public static function lookup( $filepath ) {
$parts = explode( '.', $filepath );
$ext = '.' . strtolower( array_pop( $parts ) );
return self::$types[$ext] ?? null;
}
public static function charsets_lookup( $type = null ) {
// assumes all text types are UTF-8
return $type && preg_match( '/^text\//', $type ) ? 'UTF-8' : '';
}
}

View File

@ -0,0 +1,46 @@
<?php
/**
* Parser output
*
* @private
*/
class Less_Output {
/**
* Output holder
*
* @var string[]
*/
protected $strs = [];
/**
* Adds a chunk to the stack
*
* @param string $chunk The chunk to output
* @param array|null $fileInfo The file information
* @param int $index The index
* @param bool|null $mapLines
*/
public function add( $chunk, $fileInfo = null, $index = 0, $mapLines = null ) {
$this->strs[] = $chunk;
}
/**
* Is the output empty?
*
* @return bool
*/
public function isEmpty() {
return count( $this->strs ) === 0;
}
/**
* Converts the output to string
*
* @return string
*/
public function toString() {
return implode( '', $this->strs );
}
}

View File

@ -0,0 +1,117 @@
<?php
/**
* Parser output with source map
*
* @private
*/
class Less_Output_Mapped extends Less_Output {
/**
* The source map generator
*
* @var Less_SourceMap_Generator
*/
protected $generator;
/**
* Current line
*
* @var int
*/
protected $lineNumber = 0;
/**
* Current column
*
* @var int
*/
protected $column = 0;
/**
* Array of contents map (file and its content)
*
* @var array
*/
protected $contentsMap = [];
/**
* Constructor
*
* @param array $contentsMap Array of filename to contents map
* @param Less_SourceMap_Generator $generator
*/
public function __construct( array $contentsMap, $generator ) {
$this->contentsMap = $contentsMap;
$this->generator = $generator;
}
/**
* Adds a chunk to the stack
* The $index for less.php may be different from less.js since less.php does not chunkify inputs
*
* @param string $chunk
* @param array|null $fileInfo
* @param int $index
* @param bool|null $mapLines
*/
public function add( $chunk, $fileInfo = null, $index = 0, $mapLines = null ) {
// ignore adding empty strings
if ( $chunk === '' ) {
return;
}
$sourceLines = [];
$sourceColumns = ' ';
if ( $fileInfo ) {
$url = $fileInfo['currentUri'];
if ( isset( $this->contentsMap[$url] ) ) {
$inputSource = substr( $this->contentsMap[$url], 0, $index );
$sourceLines = explode( "\n", $inputSource );
$sourceColumns = end( $sourceLines );
} else {
throw new Exception( 'Filename ' . $url . ' not in contentsMap' );
}
}
$lines = explode( "\n", $chunk );
$columns = end( $lines );
if ( $fileInfo ) {
if ( !$mapLines ) {
$this->generator->addMapping(
$this->lineNumber + 1, // generated_line
$this->column, // generated_column
count( $sourceLines ), // original_line
strlen( $sourceColumns ), // original_column
$fileInfo
);
} else {
for ( $i = 0, $count = count( $lines ); $i < $count; $i++ ) {
$this->generator->addMapping(
$this->lineNumber + $i + 1, // generated_line
$i === 0 ? $this->column : 0, // generated_column
count( $sourceLines ) + $i, // original_line
$i === 0 ? strlen( $sourceColumns ) : 0, // original_column
$fileInfo
);
}
}
}
if ( count( $lines ) === 1 ) {
$this->column += strlen( $columns );
} else {
$this->lineNumber += count( $lines ) - 1;
$this->column = strlen( $columns );
}
// add only chunk
parent::add( $chunk );
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,185 @@
<?php
/**
* Encode / Decode Base64 VLQ.
*
* @private
*/
class Less_SourceMap_Base64VLQ {
/**
* Shift
*
* @var int
*/
private $shift = 5;
/**
* Mask
*
* @var int
*/
private $mask = 0x1F; // == (1 << shift) == 0b00011111
/**
* Continuation bit
*
* @var int
*/
private $continuationBit = 0x20; // == (mask - 1 ) == 0b00100000
/**
* Char to integer map
*
* @var array
*/
private $charToIntMap = [
'A' => 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6,
'H' => 7,'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13,
'O' => 14, 'P' => 15, 'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20,
'V' => 21, 'W' => 22, 'X' => 23, 'Y' => 24, 'Z' => 25, 'a' => 26, 'b' => 27,
'c' => 28, 'd' => 29, 'e' => 30, 'f' => 31, 'g' => 32, 'h' => 33, 'i' => 34,
'j' => 35, 'k' => 36, 'l' => 37, 'm' => 38, 'n' => 39, 'o' => 40, 'p' => 41,
'q' => 42, 'r' => 43, 's' => 44, 't' => 45, 'u' => 46, 'v' => 47, 'w' => 48,
'x' => 49, 'y' => 50, 'z' => 51, 0 => 52, 1 => 53, 2 => 54, 3 => 55, 4 => 56,
5 => 57, 6 => 58, 7 => 59, 8 => 60, 9 => 61, '+' => 62, '/' => 63,
];
/**
* Integer to char map
*
* @var array
*/
private $intToCharMap = [
0 => 'A', 1 => 'B', 2 => 'C', 3 => 'D', 4 => 'E', 5 => 'F', 6 => 'G',
7 => 'H', 8 => 'I', 9 => 'J', 10 => 'K', 11 => 'L', 12 => 'M', 13 => 'N',
14 => 'O', 15 => 'P', 16 => 'Q', 17 => 'R', 18 => 'S', 19 => 'T', 20 => 'U',
21 => 'V', 22 => 'W', 23 => 'X', 24 => 'Y', 25 => 'Z', 26 => 'a', 27 => 'b',
28 => 'c', 29 => 'd', 30 => 'e', 31 => 'f', 32 => 'g', 33 => 'h', 34 => 'i',
35 => 'j', 36 => 'k', 37 => 'l', 38 => 'm', 39 => 'n', 40 => 'o', 41 => 'p',
42 => 'q', 43 => 'r', 44 => 's', 45 => 't', 46 => 'u', 47 => 'v', 48 => 'w',
49 => 'x', 50 => 'y', 51 => 'z', 52 => '0', 53 => '1', 54 => '2', 55 => '3',
56 => '4', 57 => '5', 58 => '6', 59 => '7', 60 => '8', 61 => '9', 62 => '+',
63 => '/',
];
/**
* Constructor
*/
public function __construct() {
// I leave it here for future reference
// foreach(str_split('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/') as $i => $char)
// {
// $this->charToIntMap[$char] = $i;
// $this->intToCharMap[$i] = $char;
// }
}
/**
* Convert from a two-complement value to a value where the sign bit is
* is placed in the least significant bit. For example, as decimals:
* 1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
* 2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
* We generate the value for 32 bit machines, hence -2147483648 becomes 1, not 4294967297,
* even on a 64 bit machine.
* @param int $aValue
*/
public function toVLQSigned( $aValue ) {
return 0xffffffff & ( $aValue < 0 ? ( ( -$aValue ) << 1 ) + 1 : ( $aValue << 1 ) + 0 );
}
/**
* Convert to a two-complement value from a value where the sign bit is
* is placed in the least significant bit. For example, as decimals:
* 2 (10 binary) becomes 1, 3 (11 binary) becomes -1
* 4 (100 binary) becomes 2, 5 (101 binary) becomes -2
* We assume that the value was generated with a 32 bit machine in mind.
* Hence
* 1 becomes -2147483648
* even on a 64 bit machine.
* @param int $aValue
*/
public function fromVLQSigned( $aValue ) {
return $aValue & 1 ? $this->zeroFill( ~$aValue + 2, 1 ) | ( -1 - 0x7fffffff ) : $this->zeroFill( $aValue, 1 );
}
/**
* Return the base 64 VLQ encoded value.
*
* @param int $aValue The value to encode
* @return string The encoded value
*/
public function encode( $aValue ) {
$encoded = '';
$vlq = $this->toVLQSigned( $aValue );
do
{
$digit = $vlq & $this->mask;
$vlq = $this->zeroFill( $vlq, $this->shift );
if ( $vlq > 0 ) {
$digit |= $this->continuationBit;
}
$encoded .= $this->base64Encode( $digit );
} while ( $vlq > 0 );
return $encoded;
}
/**
* Return the value decoded from base 64 VLQ.
*
* @param string $encoded The encoded value to decode
* @return int The decoded value
*/
public function decode( $encoded ) {
$vlq = 0;
$i = 0;
do
{
$digit = $this->base64Decode( $encoded[$i] );
$vlq |= ( $digit & $this->mask ) << ( $i * $this->shift );
$i++;
} while ( $digit & $this->continuationBit );
return $this->fromVLQSigned( $vlq );
}
/**
* Right shift with zero fill.
*
* @param int $a number to shift
* @param int $b number of bits to shift
* @return int
*/
public function zeroFill( $a, $b ) {
return ( $a >= 0 ) ? ( $a >> $b ) : ( $a >> $b ) & ( PHP_INT_MAX >> ( $b - 1 ) );
}
/**
* Encode single 6-bit digit as base64.
*
* @param int $number
* @return string
* @throws Exception If the number is invalid
*/
public function base64Encode( $number ) {
if ( $number < 0 || $number > 63 ) {
throw new Exception( "Invalid number \"$number\" given. Must be between 0 and 63." );
}
return $this->intToCharMap[$number];
}
/**
* Decode single 6-bit digit from base64
*
* @param string $char
* @return int
* @throws Exception If the number is invalid
*/
public function base64Decode( $char ) {
if ( !array_key_exists( $char, $this->charToIntMap ) ) {
throw new Exception( sprintf( 'Invalid base 64 digit "%s" given.', $char ) );
}
return $this->charToIntMap[$char];
}
}

View File

@ -0,0 +1,354 @@
<?php
/**
* Source map generator
*
* @private
*/
class Less_SourceMap_Generator extends Less_Configurable {
/**
* What version of source map does the generator generate?
*/
private const VERSION = 3;
/**
* Array of default options
*
* @var array
*/
protected $defaultOptions = [
// an optional source root, useful for relocating source files
// on a server or removing repeated values in the 'sources' entry.
// This value is prepended to the individual entries in the 'source' field.
'sourceRoot' => '',
// an optional name of the generated code that this source map is associated with.
'sourceMapFilename' => null,
// url of the map
'sourceMapURL' => null,
// absolute path to a file to write the map to
'sourceMapWriteTo' => null,
// output source contents?
'outputSourceFiles' => false,
// base path for filename normalization
'sourceMapRootpath' => '',
// base path for filename normalization
'sourceMapBasepath' => ''
];
/**
* The base64 VLQ encoder
*
* @var Less_SourceMap_Base64VLQ
*/
protected $encoder;
/**
* Array of mappings
*
* @var array
*/
protected $mappings = [];
/**
* The root node
*
* @var Less_Tree_Ruleset
*/
protected $root;
/**
* Array of contents map
*
* @var array
*/
protected $contentsMap = [];
/**
* File to content map
*
* @var array
*/
protected $sources = [];
protected $source_keys = [];
/**
* Constructor
*
* @param Less_Tree_Ruleset $root The root node
* @param array $contentsMap
* @param array $options Array of options
*/
public function __construct( Less_Tree_Ruleset $root, $contentsMap, $options = [] ) {
$this->root = $root;
$this->contentsMap = $contentsMap;
$this->encoder = new Less_SourceMap_Base64VLQ();
$this->SetOptions( $options );
$this->options['sourceMapRootpath'] = $this->fixWindowsPath( $this->options['sourceMapRootpath'], true );
$this->options['sourceMapBasepath'] = $this->fixWindowsPath( $this->options['sourceMapBasepath'], true );
}
/**
* Generates the CSS
*
* @return string
*/
public function generateCSS() {
$output = new Less_Output_Mapped( $this->contentsMap, $this );
// catch the output
$this->root->genCSS( $output );
$sourceMapUrl = $this->getOption( 'sourceMapURL' );
$sourceMapFilename = $this->getOption( 'sourceMapFilename' );
$sourceMapContent = $this->generateJson();
$sourceMapWriteTo = $this->getOption( 'sourceMapWriteTo' );
if ( !$sourceMapUrl && $sourceMapFilename ) {
$sourceMapUrl = $this->normalizeFilename( $sourceMapFilename );
}
// write map to a file
if ( $sourceMapWriteTo ) {
$this->saveMap( $sourceMapWriteTo, $sourceMapContent );
}
// inline the map
if ( !$sourceMapUrl ) {
$sourceMapUrl = sprintf( 'data:application/json,%s', Less_Functions::encodeURIComponent( $sourceMapContent ) );
}
if ( $sourceMapUrl ) {
$output->add( sprintf( '/*# sourceMappingURL=%s */', $sourceMapUrl ) );
}
return $output->toString();
}
/**
* Saves the source map to a file
*
* @param string $file The absolute path to a file
* @param string $content The content to write
* @throws Exception If the file could not be saved
*/
protected function saveMap( $file, $content ) {
$dir = dirname( $file );
// directory does not exist
if ( !is_dir( $dir ) ) {
// FIXME: create the dir automatically?
throw new Exception( sprintf( 'The directory "%s" does not exist. Cannot save the source map.', $dir ) );
}
// FIXME: proper saving, with dir write check!
if ( file_put_contents( $file, $content ) === false ) {
throw new Exception( sprintf( 'Cannot save the source map to "%s"', $file ) );
}
return true;
}
/**
* Normalizes the filename
*
* @param string $filename
* @return string
*/
protected function normalizeFilename( $filename ) {
$filename = $this->fixWindowsPath( $filename );
$rootpath = $this->getOption( 'sourceMapRootpath' );
$basePath = $this->getOption( 'sourceMapBasepath' );
// "Trim" the 'sourceMapBasepath' from the output filename.
if ( is_string( $basePath ) && strpos( $filename, $basePath ) === 0 ) {
$filename = substr( $filename, strlen( $basePath ) );
}
// Remove extra leading path separators.
if ( strpos( $filename, '\\' ) === 0 || strpos( $filename, '/' ) === 0 ) {
$filename = substr( $filename, 1 );
}
return $rootpath . $filename;
}
/**
* Adds a mapping
*
* @param int $generatedLine The line number in generated file
* @param int $generatedColumn The column number in generated file
* @param int $originalLine The line number in original file
* @param int $originalColumn The column number in original file
* @param array $fileInfo The original source file
*/
public function addMapping( $generatedLine, $generatedColumn, $originalLine, $originalColumn, $fileInfo ) {
$this->mappings[] = [
'generated_line' => $generatedLine,
'generated_column' => $generatedColumn,
'original_line' => $originalLine,
'original_column' => $originalColumn,
'source_file' => $fileInfo['currentUri']
];
$this->sources[$fileInfo['currentUri']] = $fileInfo['filename'];
}
/**
* Generates the JSON source map
*
* @return string
* @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#
*/
protected function generateJson() {
$sourceMap = [];
$mappings = $this->generateMappings();
// File version (always the first entry in the object) and must be a positive integer.
$sourceMap['version'] = self::VERSION;
// An optional name of the generated code that this source map is associated with.
$file = $this->getOption( 'sourceMapFilename' );
if ( $file ) {
$sourceMap['file'] = $file;
}
// An optional source root, useful for relocating source files on a server or removing repeated values in the 'sources' entry. This value is prepended to the individual entries in the 'source' field.
$root = $this->getOption( 'sourceRoot' );
if ( $root ) {
$sourceMap['sourceRoot'] = $root;
}
// A list of original sources used by the 'mappings' entry.
$sourceMap['sources'] = [];
foreach ( $this->sources as $source_uri => $source_filename ) {
$sourceMap['sources'][] = $this->normalizeFilename( $source_filename );
}
// A list of symbol names used by the 'mappings' entry.
$sourceMap['names'] = [];
// A string with the encoded mapping data.
$sourceMap['mappings'] = $mappings;
if ( $this->getOption( 'outputSourceFiles' ) ) {
// An optional list of source content, useful when the 'source' can't be hosted.
// The contents are listed in the same order as the sources above.
// 'null' may be used if some original sources should be retrieved by name.
$sourceMap['sourcesContent'] = $this->getSourcesContent();
}
// less.js compat fixes
if ( count( $sourceMap['sources'] ) && empty( $sourceMap['sourceRoot'] ) ) {
unset( $sourceMap['sourceRoot'] );
}
return json_encode( $sourceMap );
}
/**
* Returns the sources contents
*
* @return array|null
*/
protected function getSourcesContent() {
if ( empty( $this->sources ) ) {
return;
}
$content = [];
foreach ( $this->sources as $sourceFile ) {
$content[] = file_get_contents( $sourceFile );
}
return $content;
}
/**
* Generates the mappings string
*
* @return string
*/
public function generateMappings() {
if ( !count( $this->mappings ) ) {
return '';
}
$this->source_keys = array_flip( array_keys( $this->sources ) );
// group mappings by generated line number.
$groupedMap = $groupedMapEncoded = [];
foreach ( $this->mappings as $m ) {
$groupedMap[$m['generated_line']][] = $m;
}
ksort( $groupedMap );
$lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
foreach ( $groupedMap as $lineNumber => $line_map ) {
while ( ++$lastGeneratedLine < $lineNumber ) {
$groupedMapEncoded[] = ';';
}
$lineMapEncoded = [];
$lastGeneratedColumn = 0;
foreach ( $line_map as $m ) {
$mapEncoded = $this->encoder->encode( $m['generated_column'] - $lastGeneratedColumn );
$lastGeneratedColumn = $m['generated_column'];
// find the index
if ( $m['source_file'] ) {
$index = $this->findFileIndex( $m['source_file'] );
if ( $index !== false ) {
$mapEncoded .= $this->encoder->encode( $index - $lastOriginalIndex );
$lastOriginalIndex = $index;
// lines are stored 0-based in SourceMap spec version 3
$mapEncoded .= $this->encoder->encode( $m['original_line'] - 1 - $lastOriginalLine );
$lastOriginalLine = $m['original_line'] - 1;
$mapEncoded .= $this->encoder->encode( $m['original_column'] - $lastOriginalColumn );
$lastOriginalColumn = $m['original_column'];
}
}
$lineMapEncoded[] = $mapEncoded;
}
$groupedMapEncoded[] = implode( ',', $lineMapEncoded ) . ';';
}
return rtrim( implode( $groupedMapEncoded ), ';' );
}
/**
* Finds the index for the filename
*
* @param string $filename
* @return int|false
*/
protected function findFileIndex( $filename ) {
return $this->source_keys[$filename];
}
/**
* fix windows paths
* @param string $path
* @param bool $addEndSlash
* @return string
*/
public function fixWindowsPath( $path, $addEndSlash = false ) {
$slash = ( $addEndSlash ) ? '/' : '';
if ( !empty( $path ) ) {
$path = str_replace( '\\', '/', $path );
$path = rtrim( $path, '/' ) . $slash;
}
return $path;
}
}

View File

@ -0,0 +1,88 @@
<?php
/**
* Tree
*/
class Less_Tree {
public $parensInOp = false;
public $extendOnEveryPath;
public $allExtends;
public function toCSS() {
$output = new Less_Output();
$this->genCSS( $output );
return $output->toString();
}
/**
* Generate CSS by adding it to the output object
*
* @param Less_Output $output The output
* @return void
*/
public function genCSS( $output ) {
}
public function compile( $env ) {
return $this;
}
/**
* @param Less_Output $output
* @param Less_Tree_Ruleset[] $rules
*/
public static function outputRuleset( $output, $rules ) {
$ruleCnt = count( $rules );
Less_Environment::$tabLevel++;
// Compressed
if ( Less_Parser::$options['compress'] ) {
$output->add( '{' );
for ( $i = 0; $i < $ruleCnt; $i++ ) {
$rules[$i]->genCSS( $output );
}
$output->add( '}' );
Less_Environment::$tabLevel--;
return;
}
// Non-compressed
$tabSetStr = "\n" . str_repeat( Less_Parser::$options['indentation'], Less_Environment::$tabLevel - 1 );
$tabRuleStr = $tabSetStr . Less_Parser::$options['indentation'];
$output->add( " {" );
for ( $i = 0; $i < $ruleCnt; $i++ ) {
$output->add( $tabRuleStr );
$rules[$i]->genCSS( $output );
}
Less_Environment::$tabLevel--;
$output->add( $tabSetStr . '}' );
}
public function accept( $visitor ) {
}
public static function ReferencedArray( $rules ) {
foreach ( $rules as $rule ) {
if ( method_exists( $rule, 'markReferenced' ) ) {
// @phan-suppress-next-line PhanUndeclaredMethod
$rule->markReferenced();
}
}
}
/**
* Requires php 5.3+
*/
public static function __set_state( $args ) {
$class = get_called_class();
$obj = new $class( null, null, null, null );
foreach ( $args as $key => $val ) {
$obj->$key = $val;
}
return $obj;
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* @private
*/
class Less_Tree_Alpha extends Less_Tree implements Less_Tree_HasValueProperty {
public $value;
public function __construct( $val ) {
$this->value = $val;
}
// function accept( $visitor ){
// $this->value = $visitor->visit( $this->value );
//}
public function compile( $env ) {
if ( is_object( $this->value ) ) {
$this->value = $this->value->compile( $env );
}
return $this;
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$output->add( "alpha(opacity=" );
if ( is_string( $this->value ) ) {
$output->add( $this->value );
} else {
$this->value->genCSS( $output );
}
$output->add( ')' );
}
public function toCSS() {
return "alpha(opacity=" . ( is_string( $this->value ) ? $this->value : $this->value->toCSS() ) . ")";
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* @private
*/
class Less_Tree_Anonymous extends Less_Tree implements Less_Tree_HasValueProperty {
public $value;
public $quote;
public $index;
public $mapLines;
public $currentFileInfo;
/**
* @param string $value
* @param int|null $index
* @param array|null $currentFileInfo
* @param bool|null $mapLines
*/
public function __construct( $value, $index = null, $currentFileInfo = null, $mapLines = null ) {
$this->value = $value;
$this->index = $index;
$this->mapLines = $mapLines;
$this->currentFileInfo = $currentFileInfo;
}
public function compile( $env ) {
return new self( $this->value, $this->index, $this->currentFileInfo, $this->mapLines );
}
public function compare( $x ) {
if ( !is_object( $x ) ) {
return -1;
}
$left = $this->toCSS();
$right = $x->toCSS();
if ( $left === $right ) {
return 0;
}
return $left < $right ? -1 : 1;
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$output->add( $this->value, $this->currentFileInfo, $this->index, $this->mapLines );
}
public function toCSS() {
return $this->value;
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* @private
*/
class Less_Tree_Assignment extends Less_Tree implements Less_Tree_HasValueProperty {
public $key;
public $value;
public function __construct( $key, $val ) {
$this->key = $key;
$this->value = $val;
}
public function accept( $visitor ) {
$this->value = $visitor->visitObj( $this->value );
}
public function compile( $env ) {
return new self( $this->key, $this->value->compile( $env ) );
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$output->add( $this->key . '=' );
$this->value->genCSS( $output );
}
public function toCss() {
return $this->key . '=' . $this->value->toCSS();
}
}

View File

@ -0,0 +1,48 @@
<?php
/**
* @private
*/
class Less_Tree_Attribute extends Less_Tree implements Less_Tree_HasValueProperty {
public $key;
public $op;
public $value;
public function __construct( $key, $op, $value ) {
$this->key = $key;
$this->op = $op;
$this->value = $value;
}
public function compile( $env ) {
$key_obj = is_object( $this->key );
$val_obj = is_object( $this->value );
if ( !$key_obj && !$val_obj ) {
return $this;
}
return new self(
$key_obj ? $this->key->compile( $env ) : $this->key,
$this->op,
$val_obj ? $this->value->compile( $env ) : $this->value );
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$output->add( $this->toCSS() );
}
public function toCSS() {
$value = $this->key;
if ( $this->op ) {
$value .= $this->op;
$value .= ( is_object( $this->value ) ? $this->value->toCSS() : $this->value );
}
return '[' . $value . ']';
}
}

View File

@ -0,0 +1,124 @@
<?php
/**
* @private
* @see less.tree.Call in less.js 3.0.0 https://github.com/less/less.js/blob/v3.0.0/dist/less.js#L6336
*/
class Less_Tree_Call extends Less_Tree implements Less_Tree_HasValueProperty {
public $value;
public $name;
public $args;
/** @var bool */
public $mathOn;
public $index;
public $currentFileInfo;
public function __construct( $name, $args, $index, $currentFileInfo = null ) {
$this->name = $name;
$this->args = $args;
$this->mathOn = ( $name !== 'calc' );
$this->index = $index;
$this->currentFileInfo = $currentFileInfo;
}
public function accept( $visitor ) {
$this->args = $visitor->visitArray( $this->args );
}
//
// When evaluating a function call,
// we either find the function in Less_Functions,
// in which case we call it, passing the evaluated arguments,
// or we simply print it out as it literal CSS.
//
// The reason why we compile the arguments, is in the case one
// of them is a LESS variable that only PHP knows the value of,
// like: `saturate(@mycolor)`.
// The function should receive the value, not the variable.
//
public function compile( $env = null ) {
// Turn off math for calc(). https://phabricator.wikimedia.org/T331688
$currentMathContext = Less_Environment::$mathOn;
Less_Environment::$mathOn = $this->mathOn;
$args = [];
foreach ( $this->args as $a ) {
$args[] = $a->compile( $env );
}
Less_Environment::$mathOn = $currentMathContext;
$nameLC = strtolower( $this->name );
switch ( $nameLC ) {
case '%':
$nameLC = '_percent';
break;
case 'get-unit':
$nameLC = 'getunit';
break;
case 'data-uri':
$nameLC = 'datauri';
break;
case 'svg-gradient':
$nameLC = 'svggradient';
break;
}
$result = null;
if ( $nameLC === 'default' ) {
$result = Less_Tree_DefaultFunc::compile();
} else {
$func = null;
if ( method_exists( Less_Functions::class, $nameLC ) ) {
$functions = new Less_Functions( $env, $this->currentFileInfo );
$func = [ $functions, $nameLC ];
} elseif ( isset( $env->functions[$nameLC] ) && is_callable( $env->functions[$nameLC] ) ) {
$func = $env->functions[$nameLC];
}
// If the function name isn't known to LESS, output it unchanged as CSS.
if ( $func ) {
try {
$result = $func( ...$args );
} catch ( Exception $e ) {
// Preserve original trace, especially from custom functions.
// https://github.com/wikimedia/less.php/issues/38
throw new Less_Exception_Compiler(
'error evaluating function `' . $this->name . '` ' . $e->getMessage()
. ' index: ' . $this->index,
$e
);
}
}
}
if ( $result !== null ) {
return $result;
}
return new self( $this->name, $args, $this->index, $this->currentFileInfo );
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$output->add( $this->name . '(', $this->currentFileInfo, $this->index );
$args_len = count( $this->args );
for ( $i = 0; $i < $args_len; $i++ ) {
$this->args[$i]->genCSS( $output );
if ( $i + 1 < $args_len ) {
$output->add( ', ' );
}
}
$output->add( ')' );
}
// public function toCSS(){
// return $this->compile()->toCSS();
//}
}

View File

@ -0,0 +1,234 @@
<?php
/**
* @private
*/
class Less_Tree_Color extends Less_Tree {
public $rgb;
public $alpha;
public $isTransparentKeyword;
public $value;
public function __construct( $rgb, $a = 1, $isTransparentKeyword = null ) {
if ( $isTransparentKeyword ) {
$this->rgb = $rgb;
$this->alpha = $a;
$this->isTransparentKeyword = true;
return;
}
$this->rgb = [];
if ( is_array( $rgb ) ) {
$this->rgb = $rgb;
} elseif ( strlen( $rgb ) == 6 ) {
foreach ( str_split( $rgb, 2 ) as $c ) {
$this->rgb[] = hexdec( $c );
}
} else {
foreach ( str_split( $rgb, 1 ) as $c ) {
$this->rgb[] = hexdec( $c . $c );
}
}
$this->alpha = is_numeric( $a ) ? $a : 1;
}
public function luma() {
$r = $this->rgb[0] / 255;
$g = $this->rgb[1] / 255;
$b = $this->rgb[2] / 255;
$r = ( $r <= 0.03928 ) ? $r / 12.92 : pow( ( ( $r + 0.055 ) / 1.055 ), 2.4 );
$g = ( $g <= 0.03928 ) ? $g / 12.92 : pow( ( ( $g + 0.055 ) / 1.055 ), 2.4 );
$b = ( $b <= 0.03928 ) ? $b / 12.92 : pow( ( ( $b + 0.055 ) / 1.055 ), 2.4 );
return 0.2126 * $r + 0.7152 * $g + 0.0722 * $b;
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$output->add( $this->toCSS() );
}
public function toCSS( $doNotCompress = false ) {
$compress = Less_Parser::$options['compress'] && !$doNotCompress;
$alpha = Less_Functions::fround( $this->alpha );
//
// If we have some transparency, the only way to represent it
// is via `rgba`. Otherwise, we use the hex representation,
// which has better compatibility with older browsers.
// Values are capped between `0` and `255`, rounded and zero-padded.
//
if ( $alpha < 1 ) {
if ( ( $alpha === 0 || $alpha === 0.0 ) && isset( $this->isTransparentKeyword ) && $this->isTransparentKeyword ) {
return 'transparent';
}
$values = [];
foreach ( $this->rgb as $c ) {
$values[] = Less_Functions::clamp( round( $c ), 255 );
}
$values[] = $alpha;
$glue = ( $compress ? ',' : ', ' );
return "rgba(" . implode( $glue, $values ) . ")";
} else {
$color = $this->toRGB();
if ( $compress ) {
// Convert color to short format
if ( $color[1] === $color[2] && $color[3] === $color[4] && $color[5] === $color[6] ) {
$color = '#' . $color[1] . $color[3] . $color[5];
}
}
return $color;
}
}
//
// Operations have to be done per-channel, if not,
// channels will spill onto each other. Once we have
// our result, in the form of an integer triplet,
// we create a new Color node to hold the result.
//
/**
* @param string $op
* @param self $other
*/
public function operate( $op, $other ) {
$rgb = [];
$alpha = $this->alpha * ( 1 - $other->alpha ) + $other->alpha;
for ( $c = 0; $c < 3; $c++ ) {
$rgb[$c] = Less_Functions::operate( $op, $this->rgb[$c], $other->rgb[$c] );
}
return new self( $rgb, $alpha );
}
public function toRGB() {
return $this->toHex( $this->rgb );
}
public function toHSL() {
$r = $this->rgb[0] / 255;
$g = $this->rgb[1] / 255;
$b = $this->rgb[2] / 255;
$a = $this->alpha;
$max = max( $r, $g, $b );
$min = min( $r, $g, $b );
$l = ( $max + $min ) / 2;
$d = $max - $min;
$h = $s = 0;
if ( $max !== $min ) {
$s = $l > 0.5 ? $d / ( 2 - $max - $min ) : $d / ( $max + $min );
switch ( $max ) {
case $r:
$h = ( $g - $b ) / $d + ( $g < $b ? 6 : 0 );
break;
case $g:
$h = ( $b - $r ) / $d + 2;
break;
case $b:
$h = ( $r - $g ) / $d + 4;
break;
}
$h /= 6;
}
return [ 'h' => $h * 360, 's' => $s, 'l' => $l, 'a' => $a ];
}
// Adapted from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
public function toHSV() {
$r = $this->rgb[0] / 255;
$g = $this->rgb[1] / 255;
$b = $this->rgb[2] / 255;
$a = $this->alpha;
$max = max( $r, $g, $b );
$min = min( $r, $g, $b );
$v = $max;
$d = $max - $min;
if ( $max === 0 ) {
$s = 0;
} else {
$s = $d / $max;
}
$h = 0;
if ( $max !== $min ) {
switch ( $max ) {
case $r:
$h = ( $g - $b ) / $d + ( $g < $b ? 6 : 0 );
break;
case $g:
$h = ( $b - $r ) / $d + 2;
break;
case $b:
$h = ( $r - $g ) / $d + 4;
break;
}
$h /= 6;
}
return [ 'h' => $h * 360, 's' => $s, 'v' => $v, 'a' => $a ];
}
public function toARGB() {
$argb = array_merge( (array)Less_Parser::round( $this->alpha * 255 ), $this->rgb );
return $this->toHex( $argb );
}
public function compare( $x ) {
if ( !property_exists( $x, 'rgb' ) ) {
return -1;
}
return ( $x->rgb[0] === $this->rgb[0] &&
$x->rgb[1] === $this->rgb[1] &&
$x->rgb[2] === $this->rgb[2] &&
$x->alpha === $this->alpha ) ? 0 : -1;
}
public function toHex( $v ) {
$ret = '#';
foreach ( $v as $c ) {
$c = Less_Functions::clamp( Less_Parser::round( $c ), 255 );
if ( $c < 16 ) {
$ret .= '0';
}
$ret .= dechex( $c );
}
return $ret;
}
/**
* @param string $keyword
*/
public static function fromKeyword( $keyword ) {
$c = $keyword = strtolower( $keyword );
if ( Less_Colors::hasOwnProperty( $keyword ) ) {
// detect named color
$c = new self( substr( Less_Colors::color( $keyword ), 1 ) );
}
if ( $keyword === 'transparent' ) {
$c = new self( [ 0, 0, 0 ], 0, true );
}
if ( isset( $c ) && is_object( $c ) ) {
$c->value = $keyword;
return $c;
}
}
}

View File

@ -0,0 +1,42 @@
<?php
/**
* @private
*/
class Less_Tree_Comment extends Less_Tree implements Less_Tree_HasValueProperty {
public $value;
public $silent;
public $isReferenced;
public $currentFileInfo;
public function __construct( $value, $silent, $index = null, $currentFileInfo = null ) {
$this->value = $value;
$this->silent = (bool)$silent;
$this->currentFileInfo = $currentFileInfo;
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
// if( $this->debugInfo ){
//$output->add( tree.debugInfo($env, $this), $this->currentFileInfo, $this->index);
//}
$output->add( trim( $this->value ) );// TODO shouldn't need to trim, we shouldn't grab the \n
}
public function toCSS() {
return Less_Parser::$options['compress'] ? '' : $this->value;
}
public function isSilent() {
$isReference = ( $this->currentFileInfo && isset( $this->currentFileInfo['reference'] ) && ( !isset( $this->isReferenced ) || !$this->isReferenced ) );
$isCompressed = Less_Parser::$options['compress'] && !preg_match( '/^\/\*!/', $this->value );
return $this->silent || $isReference || $isCompressed;
}
public function markReferenced() {
$this->isReferenced = true;
}
}

View File

@ -0,0 +1,67 @@
<?php
/**
* @private
*/
class Less_Tree_Condition extends Less_Tree {
public $op;
public $lvalue;
public $rvalue;
public $index;
public $negate;
public function __construct( $op, $l, $r, $i = 0, $negate = false ) {
$this->op = trim( $op );
$this->lvalue = $l;
$this->rvalue = $r;
$this->index = $i;
$this->negate = $negate;
}
public function accept( $visitor ) {
$this->lvalue = $visitor->visitObj( $this->lvalue );
$this->rvalue = $visitor->visitObj( $this->rvalue );
}
public function compile( $env ) {
$a = $this->lvalue->compile( $env );
$b = $this->rvalue->compile( $env );
switch ( $this->op ) {
case 'and':
$result = $a && $b;
break;
case 'or':
$result = $a || $b;
break;
default:
if ( Less_Parser::is_method( $a, 'compare' ) ) {
$result = $a->compare( $b );
} elseif ( Less_Parser::is_method( $b, 'compare' ) ) {
$result = $b->compare( $a );
} else {
throw new Less_Exception_Compiler( 'Unable to perform comparison', null, $this->index );
}
switch ( $result ) {
case -1:
$result = $this->op === '<' || $this->op === '=<' || $this->op === '<=';
break;
case 0:
$result = $this->op === '=' || $this->op === '>=' || $this->op === '=<' || $this->op === '<=';
break;
case 1:
$result = $this->op === '>' || $this->op === '>=';
break;
}
break;
}
return $this->negate ? !$result : $result;
}
}

View File

@ -0,0 +1,30 @@
<?php
/**
* @private
*/
class Less_Tree_DefaultFunc {
private static $error_;
private static $value_;
public static function compile() {
if ( self::$error_ ) {
throw new Exception( self::$error_ );
}
if ( self::$value_ !== null ) {
return self::$value_ ? new Less_Tree_Keyword( 'true' ) : new Less_Tree_Keyword( 'false' );
}
}
public static function value( $v ) {
self::$value_ = $v;
}
public static function error( $e ) {
self::$error_ = $e;
}
public static function reset() {
self::$value_ = self::$error_ = null;
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* @private
*/
class Less_Tree_DetachedRuleset extends Less_Tree {
public $ruleset;
public $frames;
public function __construct( $ruleset, $frames = null ) {
$this->ruleset = $ruleset;
$this->frames = $frames;
}
public function accept( $visitor ) {
$this->ruleset = $visitor->visitObj( $this->ruleset );
}
public function compile( $env ) {
if ( $this->frames ) {
$frames = $this->frames;
} else {
$frames = $env->frames;
}
return new self( $this->ruleset, $frames );
}
public function callEval( $env ) {
if ( $this->frames ) {
return $this->ruleset->compile( $env->copyEvalEnv( array_merge( $this->frames, $env->frames ) ) );
}
return $this->ruleset->compile( $env );
}
}

View File

@ -0,0 +1,191 @@
<?php
/**
* @private
*/
class Less_Tree_Dimension extends Less_Tree implements Less_Tree_HasValueProperty {
/** @var float */
public $value;
public $unit;
public function __construct( $value, $unit = null ) {
$this->value = floatval( $value );
if ( $unit instanceof Less_Tree_Unit ) {
$this->unit = $unit;
} elseif ( $unit ) {
$this->unit = new Less_Tree_Unit( [ $unit ] );
} else {
$this->unit = new Less_Tree_Unit();
}
}
public function accept( $visitor ) {
$this->unit = $visitor->visitObj( $this->unit );
}
public function toColor() {
return new Less_Tree_Color( [ $this->value, $this->value, $this->value ] );
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
if ( Less_Parser::$options['strictUnits'] && !$this->unit->isSingular() ) {
throw new Less_Exception_Compiler( "Multiple units in dimension. Correct the units or use the unit function. Bad unit: " . $this->unit->toString() );
}
$value = Less_Functions::fround( $this->value );
$strValue = (string)$value;
if ( $value !== 0 && $value < 0.000001 && $value > -0.000001 ) {
// would be output 1e-6 etc.
$strValue = number_format( (float)$strValue, 10 );
$strValue = preg_replace( '/\.?0+$/', '', $strValue );
}
if ( Less_Parser::$options['compress'] ) {
// Zero values doesn't need a unit
if ( $value === 0 && $this->unit->isLength() ) {
$output->add( $strValue );
return;
}
// Float values doesn't need a leading zero
if ( $value > 0 && $value < 1 && $strValue[0] === '0' ) {
$strValue = substr( $strValue, 1 );
}
}
$output->add( $strValue );
$this->unit->genCSS( $output );
}
public function __toString() {
return $this->toCSS();
}
// In an operation between two Dimensions,
// we default to the first Dimension's unit,
// so `1px + 2em` will yield `3px`.
/**
* @param string $op
* @param self $other
*/
public function operate( $op, $other ) {
$value = Less_Functions::operate( $op, $this->value, $other->value );
$unit = clone $this->unit;
if ( $op === '+' || $op === '-' ) {
if ( !$unit->numerator && !$unit->denominator ) {
$unit->numerator = $other->unit->numerator;
$unit->denominator = $other->unit->denominator;
} elseif ( !$other->unit->numerator && !$other->unit->denominator ) {
// do nothing
} else {
$other = $other->convertTo( $this->unit->usedUnits() );
if ( Less_Parser::$options['strictUnits'] && $other->unit->toString() !== $unit->toCSS() ) {
throw new Less_Exception_Compiler( "Incompatible units. Change the units or use the unit function. Bad units: '" . $unit->toString() . "' and " . $other->unit->toString() . "'." );
}
$value = Less_Functions::operate( $op, $this->value, $other->value );
}
} elseif ( $op === '*' ) {
$unit->numerator = array_merge( $unit->numerator, $other->unit->numerator );
$unit->denominator = array_merge( $unit->denominator, $other->unit->denominator );
sort( $unit->numerator );
sort( $unit->denominator );
$unit->cancel();
} elseif ( $op === '/' ) {
$unit->numerator = array_merge( $unit->numerator, $other->unit->denominator );
$unit->denominator = array_merge( $unit->denominator, $other->unit->numerator );
sort( $unit->numerator );
sort( $unit->denominator );
$unit->cancel();
}
return new self( $value, $unit );
}
public function compare( $other ) {
if ( $other instanceof self ) {
if ( $this->unit->isEmpty() || $other->unit->isEmpty() ) {
$a = $this;
$b = $other;
} else {
$a = $this->unify();
$b = $other->unify();
if ( $a->unit->compare( $b->unit ) !== 0 ) {
return -1;
}
}
$aValue = $a->value;
$bValue = $b->value;
if ( $bValue > $aValue ) {
return -1;
} elseif ( $bValue < $aValue ) {
return 1;
} else {
return 0;
}
} else {
return -1;
}
}
public function unify() {
return $this->convertTo( [ 'length' => 'px', 'duration' => 's', 'angle' => 'rad' ] );
}
public function convertTo( $conversions ) {
$value = $this->value;
$unit = clone $this->unit;
if ( is_string( $conversions ) ) {
$derivedConversions = [];
foreach ( Less_Tree_UnitConversions::$groups as $i ) {
if ( isset( Less_Tree_UnitConversions::${$i}[$conversions] ) ) {
$derivedConversions = [ $i => $conversions ];
}
}
$conversions = $derivedConversions;
}
foreach ( $conversions as $groupName => $targetUnit ) {
$group = Less_Tree_UnitConversions::${$groupName};
// numerator
foreach ( $unit->numerator as $i => $atomicUnit ) {
$atomicUnit = $unit->numerator[$i];
if ( !isset( $group[$atomicUnit] ) ) {
continue;
}
$value *= $group[$atomicUnit] / $group[$targetUnit];
$unit->numerator[$i] = $targetUnit;
}
// denominator
foreach ( $unit->denominator as $i => $atomicUnit ) {
$atomicUnit = $unit->denominator[$i];
if ( !isset( $group[$atomicUnit] ) ) {
continue;
}
$value /= $group[$atomicUnit] / $group[$targetUnit];
$unit->denominator[$i] = $targetUnit;
}
}
$unit->cancel();
return new self( $value, $unit );
}
}

View File

@ -0,0 +1,123 @@
<?php
/**
* @private
*/
class Less_Tree_Directive extends Less_Tree implements Less_Tree_HasValueProperty {
public $name;
public $value;
public $rules;
public $index;
public $isReferenced;
public $isRooted;
public $currentFileInfo;
public $debugInfo;
public function __construct( $name, $value = null, $rules = null, $index = null, $isRooted = false, $currentFileInfo = null, $debugInfo = null ) {
$this->name = $name;
$this->value = $value;
if ( $rules ) {
if ( is_array( $rules ) ) {
$this->rules = $rules;
} else {
$this->rules = [ $rules ];
$this->rules[0]->selectors = $this->emptySelectors();
}
foreach ( $this->rules as $rule ) {
$rule->allowImports = true;
}
}
$this->index = $index;
$this->isRooted = $isRooted;
$this->currentFileInfo = $currentFileInfo;
$this->debugInfo = $debugInfo;
}
public function accept( $visitor ) {
if ( $this->rules ) {
$this->rules = $visitor->visitArray( $this->rules );
}
if ( $this->value ) {
$this->value = $visitor->visitObj( $this->value );
}
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$value = $this->value;
$rules = $this->rules;
$output->add( $this->name, $this->currentFileInfo, $this->index );
if ( $this->value ) {
$output->add( ' ' );
$this->value->genCSS( $output );
}
if ( $this->rules ) {
Less_Tree::outputRuleset( $output, $this->rules );
} else {
$output->add( ';' );
}
}
public function compile( $env ) {
$value = $this->value;
$rules = $this->rules;
// Media stored inside other directive should not bubble over it
// backup media bubbling information
$mediaPathBackup = $env->mediaPath;
$mediaPBlocksBackup = $env->mediaBlocks;
// Deleted media bubbling information
$env->mediaPath = [];
$env->mediaBlocks = [];
if ( $value ) {
$value = $value->compile( $env );
}
if ( $rules ) {
// Assuming that there is only one rule at this point - that is how parser constructs the rule
$rules = $rules[0]->compile( $env );
$rules->root = true;
}
// Restore media bubbling information
$env->mediaPath = $mediaPathBackup;
$env->mediaBlocks = $mediaPBlocksBackup;
return new self( $this->name, $value, $rules, $this->index, $this->isRooted, $this->currentFileInfo, $this->debugInfo );
}
public function variable( $name ) {
if ( $this->rules ) {
return $this->rules[0]->variable( $name );
}
}
public function find( $selector ) {
if ( $this->rules ) {
return $this->rules[0]->find( $selector, $this );
}
}
// rulesets: function () { if (this.rules) return tree.Ruleset.prototype.rulesets.apply(this.rules); },
public function markReferenced() {
$this->isReferenced = true;
if ( $this->rules ) {
Less_Tree::ReferencedArray( $this->rules );
}
}
public function emptySelectors() {
$el = new Less_Tree_Element( '', '&', $this->index, $this->currentFileInfo );
$sels = [ new Less_Tree_Selector( [ $el ], [], null, $this->index, $this->currentFileInfo ) ];
$sels[0]->mediaEmpty = true;
return $sels;
}
}

View File

@ -0,0 +1,72 @@
<?php
/**
* @private
*/
class Less_Tree_Element extends Less_Tree implements Less_Tree_HasValueProperty {
/** @var string */
public $combinator;
/** @var bool Whether combinator is null (represented by empty string) or child (single space) */
public $combinatorIsEmptyOrWhitespace;
/** @var string|Less_Tree */
public $value;
public $index;
public $currentFileInfo;
public $value_is_object = false;
/**
* @param null|string $combinator
* @param string|Less_Tree $value
* @param int|null $index
* @param array|null $currentFileInfo
*/
public function __construct( $combinator, $value, $index = null, $currentFileInfo = null ) {
$this->value = $value;
$this->value_is_object = is_object( $value );
// see less-2.5.3.js#Combinator
$this->combinator = $combinator ?? '';
$this->combinatorIsEmptyOrWhitespace = ( $combinator === null || trim( $combinator ) === '' );
$this->index = $index;
$this->currentFileInfo = $currentFileInfo;
}
public function accept( $visitor ) {
if ( $this->value_is_object ) { // object or string
$this->value = $visitor->visitObj( $this->value );
}
}
public function compile( $env ) {
return new self(
$this->combinator,
( $this->value_is_object ? $this->value->compile( $env ) : $this->value ),
$this->index,
$this->currentFileInfo
);
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$output->add( $this->toCSS(), $this->currentFileInfo, $this->index );
}
public function toCSS() {
if ( $this->value_is_object ) {
$value = $this->value->toCSS();
} else {
$value = $this->value;
}
if ( $value === '' && $this->combinator === '&' ) {
return '';
}
return Less_Environment::$_outputMap[$this->combinator] . $value;
}
}

View File

@ -0,0 +1,90 @@
<?php
/**
* @private
*/
class Less_Tree_Expression extends Less_Tree implements Less_Tree_HasValueProperty {
/** @var Less_Tree[] */
public $value = [];
public $parens = false;
public function __construct( $value, $parens = null ) {
$this->value = $value;
$this->parens = $parens;
}
public function accept( $visitor ) {
$this->value = $visitor->visitArray( $this->value );
}
public function compile( $env ) {
$doubleParen = false;
if ( $this->parens && !$this->parensInOp ) {
Less_Environment::$parensStack++;
}
$returnValue = null;
if ( $this->value ) {
$count = count( $this->value );
if ( $count > 1 ) {
$ret = [];
foreach ( $this->value as $e ) {
$ret[] = $e->compile( $env );
}
$returnValue = new self( $ret );
} else {
if ( ( $this->value[0] instanceof self ) && $this->value[0]->parens && !$this->value[0]->parensInOp ) {
$doubleParen = true;
}
$returnValue = $this->value[0]->compile( $env );
}
} else {
$returnValue = $this;
}
if ( $this->parens ) {
if ( !$this->parensInOp ) {
Less_Environment::$parensStack--;
} elseif ( !Less_Environment::isMathOn() && !$doubleParen ) {
$returnValue = new Less_Tree_Paren( $returnValue );
}
}
return $returnValue;
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$val_len = count( $this->value );
for ( $i = 0; $i < $val_len; $i++ ) {
$this->value[$i]->genCSS( $output );
if ( $i + 1 < $val_len ) {
$output->add( ' ' );
}
}
}
public function throwAwayComments() {
if ( is_array( $this->value ) ) {
$new_value = [];
foreach ( $this->value as $v ) {
if ( $v instanceof Less_Tree_Comment ) {
continue;
}
$new_value[] = $v;
}
$this->value = $new_value;
}
}
}

View File

@ -0,0 +1,81 @@
<?php
/**
* @private
*/
class Less_Tree_Extend extends Less_Tree {
public $selector;
public $option;
public $index;
public $selfSelectors = [];
public $allowBefore;
public $allowAfter;
public $firstExtendOnThisSelectorPath;
public $ruleset;
public $object_id;
public $parent_ids = [];
/**
* @param Less_Tree_Selector $selector
* @param string $option
* @param int $index
*/
public function __construct( $selector, $option, $index ) {
static $i = 0;
$this->selector = $selector;
$this->option = $option;
$this->index = $index;
switch ( $option ) {
case "all":
$this->allowBefore = true;
$this->allowAfter = true;
break;
default:
$this->allowBefore = false;
$this->allowAfter = false;
break;
}
// This must use a string (instead of int) so that array_merge()
// preserves keys on arrays that use IDs in their keys.
$this->object_id = 'id_' . $i++;
$this->parent_ids = [
$this->object_id => true
];
}
public function accept( $visitor ) {
$this->selector = $visitor->visitObj( $this->selector );
}
public function compile( $env ) {
Less_Parser::$has_extends = true;
$this->selector = $this->selector->compile( $env );
return $this;
// return new self( $this->selector->compile($env), $this->option, $this->index);
}
public function clone() {
return new self( $this->selector, $this->option, $this->index );
}
public function findSelfSelectors( $selectors ) {
$selfElements = [];
for ( $i = 0, $selectors_len = count( $selectors ); $i < $selectors_len; $i++ ) {
$selectorElements = $selectors[$i]->elements;
// duplicate the logic in genCSS function inside the selector node.
// future TODO - move both logics into the selector joiner visitor
if ( $i && $selectorElements && $selectorElements[0]->combinator === "" ) {
$selectorElements[0]->combinator = ' ';
}
$selfElements = array_merge( $selfElements, $selectors[$i]->elements );
}
$this->selfSelectors = [ new Less_Tree_Selector( $selfElements ) ];
}
}

View File

@ -0,0 +1,8 @@
<?php
/**
* @private
* @property mixed $value
*/
interface Less_Tree_HasValueProperty {
}

View File

@ -0,0 +1,299 @@
<?php
/**
* CSS `@import` node
*
* The general strategy here is that we don't want to wait
* for the parsing to be completed, before we start importing
* the file. That's because in the context of a browser,
* most of the time will be spent waiting for the server to respond.
*
* On creation, we push the import path to our import queue, though
* `import,push`, we also pass it a callback, which it'll call once
* the file has been fetched, and parsed.
*
* @private
*/
class Less_Tree_Import extends Less_Tree {
public $options;
public $index;
public $path;
public $features;
public $currentFileInfo;
public $css;
public $skip;
public $root;
public function __construct( $path, $features, $options, $index, $currentFileInfo = null ) {
$this->options = $options;
$this->index = $index;
$this->path = $path;
$this->features = $features;
$this->currentFileInfo = $currentFileInfo;
if ( is_array( $options ) ) {
$this->options += [ 'inline' => false ];
if ( isset( $this->options['less'] ) || $this->options['inline'] ) {
$this->css = !isset( $this->options['less'] ) || !$this->options['less'] || $this->options['inline'];
} else {
$pathValue = $this->getPath();
// Leave any ".css" file imports as literals for the browser.
// Also leave any remote HTTP resources as literals regardless of whether
// they contain ".css" in their filename.
if ( $pathValue && preg_match( '/^(https?:)?\/\/|\.css$/i', $pathValue ) ) {
$this->css = true;
}
}
}
}
//
// The actual import node doesn't return anything, when converted to CSS.
// The reason is that it's used at the evaluation stage, so that the rules
// it imports can be treated like any other rules.
//
// In `eval`, we make sure all Import nodes get evaluated, recursively, so
// we end up with a flat structure, which can easily be imported in the parent
// ruleset.
//
public function accept( $visitor ) {
if ( $this->features ) {
$this->features = $visitor->visitObj( $this->features );
}
$this->path = $visitor->visitObj( $this->path );
if ( !$this->options['inline'] && $this->root ) {
$this->root = $visitor->visit( $this->root );
}
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
if ( $this->css ) {
$output->add( '@import ', $this->currentFileInfo, $this->index );
$this->path->genCSS( $output );
if ( $this->features ) {
$output->add( ' ' );
$this->features->genCSS( $output );
}
$output->add( ';' );
}
}
public function toCSS() {
$features = $this->features ? ' ' . $this->features->toCSS() : '';
if ( $this->css ) {
return "@import " . $this->path->toCSS() . $features . ";\n";
} else {
return "";
}
}
/**
* @return string|null
*/
public function getPath() {
if ( $this->path instanceof Less_Tree_Quoted ) {
$path = $this->path->value;
$path = ( isset( $this->css ) || preg_match( '/(\.[a-z]*$)|([\?;].*)$/', $path ) ) ? $path : $path . '.less';
// During the first pass, Less_Tree_Url may contain a Less_Tree_Variable (not yet expanded),
// and thus has no value property defined yet. Return null until we reach the next phase.
// https://github.com/wikimedia/less.php/issues/29
} elseif ( $this->path instanceof Less_Tree_Url && !( $this->path->value instanceof Less_Tree_Variable ) ) {
$path = $this->path->value->value;
} else {
return null;
}
// remove query string and fragment
return preg_replace( '/[\?#][^\?]*$/', '', $path );
}
public function compileForImport( $env ) {
return new self( $this->path->compile( $env ), $this->features, $this->options, $this->index, $this->currentFileInfo );
}
public function compilePath( $env ) {
$path = $this->path->compile( $env );
$rootpath = '';
if ( $this->currentFileInfo && $this->currentFileInfo['rootpath'] ) {
$rootpath = $this->currentFileInfo['rootpath'];
}
if ( !( $path instanceof Less_Tree_Url ) ) {
if ( $rootpath ) {
$pathValue = $path->value;
// Add the base path if the import is relative
if ( $pathValue && Less_Environment::isPathRelative( $pathValue ) ) {
$path->value = $this->currentFileInfo['uri_root'] . $pathValue;
}
}
$path->value = Less_Environment::normalizePath( $path->value );
}
return $path;
}
public function compile( $env ) {
$evald = $this->compileForImport( $env );
// get path & uri
$callback = Less_Parser::$options['import_callback'];
$path_and_uri = is_callable( $callback ) ? $callback( $evald ) : null;
if ( !$path_and_uri ) {
$path_and_uri = $evald->PathAndUri();
}
if ( $path_and_uri ) {
list( $full_path, $uri ) = $path_and_uri;
} else {
$full_path = $uri = $evald->getPath();
}
// import once
if ( $evald->skip( $full_path, $env ) ) {
return [];
}
'@phan-var string $full_path';
if ( $this->options['inline'] ) {
// todo needs to reference css file not import
//$contents = new Less_Tree_Anonymous($this->root, 0, array('filename'=>$this->importedFilename), true );
Less_Parser::AddParsedFile( $full_path );
$contents = new Less_Tree_Anonymous( file_get_contents( $full_path ), 0, [], true );
if ( $this->features ) {
return new Less_Tree_Media( [ $contents ], $this->features->value );
}
return [ $contents ];
}
// optional (need to be before "CSS" to support optional CSS imports. CSS should be checked only if empty($this->currentFileInfo))
if ( isset( $this->options['optional'] ) && $this->options['optional'] && !file_exists( $full_path ) && ( !$evald->css || !empty( $this->currentFileInfo ) ) ) {
return [];
}
// css ?
if ( $evald->css ) {
$features = ( $evald->features ? $evald->features->compile( $env ) : null );
return new self( $this->compilePath( $env ), $features, $this->options, $this->index );
}
return $this->ParseImport( $full_path, $uri, $env );
}
/**
* Using the import directories, get the full absolute path and uri of the import
*/
public function PathAndUri() {
$evald_path = $this->getPath();
if ( $evald_path ) {
$import_dirs = [];
if ( Less_Environment::isPathRelative( $evald_path ) ) {
// if the path is relative, the file should be in the current directory
if ( $this->currentFileInfo ) {
$import_dirs[ $this->currentFileInfo['currentDirectory'] ] = $this->currentFileInfo['uri_root'];
}
} else {
// otherwise, the file should be relative to the server root
if ( $this->currentFileInfo ) {
$import_dirs[ $this->currentFileInfo['entryPath'] ] = $this->currentFileInfo['entryUri'];
}
// if the user supplied entryPath isn't the actual root
$import_dirs[ $_SERVER['DOCUMENT_ROOT'] ] = '';
}
// always look in user supplied import directories
$import_dirs = array_merge( $import_dirs, Less_Parser::$options['import_dirs'] );
foreach ( $import_dirs as $rootpath => $rooturi ) {
if ( is_callable( $rooturi ) ) {
$res = $rooturi( $evald_path );
if ( $res && is_string( $res[0] ) ) {
return [
Less_Environment::normalizePath( $res[0] ),
Less_Environment::normalizePath( $res[1] ?? dirname( $evald_path ) )
];
}
} elseif ( !empty( $rootpath ) ) {
$path = rtrim( $rootpath, '/\\' ) . '/' . ltrim( $evald_path, '/\\' );
if ( file_exists( $path ) ) {
return [
Less_Environment::normalizePath( $path ),
Less_Environment::normalizePath( dirname( $rooturi . $evald_path ) )
];
}
if ( file_exists( $path . '.less' ) ) {
return [
Less_Environment::normalizePath( $path . '.less' ),
Less_Environment::normalizePath( dirname( $rooturi . $evald_path . '.less' ) )
];
}
}
}
}
}
/**
* Parse the import url and return the rules
*
* @param string $full_path
* @param string|null $uri
* @param mixed $env
* @return Less_Tree_Media|array
*/
public function ParseImport( $full_path, $uri, $env ) {
$import_env = clone $env;
if ( ( isset( $this->options['reference'] ) && $this->options['reference'] ) || isset( $this->currentFileInfo['reference'] ) ) {
$import_env->currentFileInfo['reference'] = true;
}
if ( ( isset( $this->options['multiple'] ) && $this->options['multiple'] ) ) {
$import_env->importMultiple = true;
}
$parser = new Less_Parser( $import_env );
$root = $parser->parseFile( $full_path, $uri, true );
$ruleset = new Less_Tree_Ruleset( null, $root->rules );
$ruleset->evalImports( $import_env );
return $this->features ? new Less_Tree_Media( $ruleset->rules, $this->features->value ) : $ruleset->rules;
}
/**
* Should the import be skipped?
*
* @param string|null $path
* @param Less_Environment $env
* @return bool|null
*/
private function skip( $path, $env ) {
$path = Less_Parser::AbsPath( $path, true );
if ( $path && Less_Parser::FileParsed( $path ) ) {
if ( isset( $this->currentFileInfo['reference'] ) ) {
return true;
}
return !isset( $this->options['multiple'] ) && !$env->importMultiple;
}
}
}

View File

@ -0,0 +1,26 @@
<?php
/**
* @private
*/
class Less_Tree_JavaScript extends Less_Tree {
public $escaped;
public $expression;
public $index;
/**
* @param string $string
* @param int $index
* @param bool $escaped
*/
public function __construct( $string, $index, $escaped ) {
$this->escaped = $escaped;
$this->expression = $string;
$this->index = $index;
}
public function compile( $env ) {
return new Less_Tree_Anonymous( '/* Sorry, can not do JavaScript evaluation in PHP... :( */' );
}
}

View File

@ -0,0 +1,35 @@
<?php
/**
* @private
*/
class Less_Tree_Keyword extends Less_Tree implements Less_Tree_HasValueProperty {
/** @var string */
public $value;
/**
* @param string $value
*/
public function __construct( $value ) {
$this->value = $value;
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
if ( $this->value === '%' ) {
throw new Less_Exception_Compiler( "Invalid % without number" );
}
$output->add( $this->value );
}
public function compare( $other ) {
if ( $other instanceof self ) {
return $other->value === $this->value ? 0 : 1;
} else {
return -1;
}
}
}

View File

@ -0,0 +1,183 @@
<?php
/**
* @private
*/
class Less_Tree_Media extends Less_Tree {
public $features;
public $rules;
public $index;
public $currentFileInfo;
public $isReferenced;
public function __construct( $value = [], $features = [], $index = null, $currentFileInfo = null ) {
$this->index = $index;
$this->currentFileInfo = $currentFileInfo;
$selectors = $this->emptySelectors();
$this->features = new Less_Tree_Value( $features );
$this->rules = [ new Less_Tree_Ruleset( $selectors, $value ) ];
$this->rules[0]->allowImports = true;
}
public function accept( $visitor ) {
$this->features = $visitor->visitObj( $this->features );
$this->rules = $visitor->visitArray( $this->rules );
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$output->add( '@media ', $this->currentFileInfo, $this->index );
$this->features->genCSS( $output );
Less_Tree::outputRuleset( $output, $this->rules );
}
/**
* @param Less_Environment $env
* @return self|Less_Tree_Ruleset
* @see less-2.5.3.js#Media.prototype.eval
*/
public function compile( $env ) {
$media = new self( [], [], $this->index, $this->currentFileInfo );
$strictMathBypass = false;
if ( Less_Parser::$options['strictMath'] === false ) {
$strictMathBypass = true;
Less_Parser::$options['strictMath'] = true;
}
$media->features = $this->features->compile( $env );
if ( $strictMathBypass ) {
Less_Parser::$options['strictMath'] = false;
}
$env->mediaPath[] = $media;
$env->mediaBlocks[] = $media;
array_unshift( $env->frames, $this->rules[0] );
$media->rules = [ $this->rules[0]->compile( $env ) ];
array_shift( $env->frames );
array_pop( $env->mediaPath );
return !$env->mediaPath ? $media->compileTop( $env ) : $media->compileNested( $env );
}
public function variable( $name ) {
return $this->rules[0]->variable( $name );
}
public function find( $selector ) {
return $this->rules[0]->find( $selector, $this );
}
public function emptySelectors() {
$el = new Less_Tree_Element( '', '&', $this->index, $this->currentFileInfo );
$sels = [ new Less_Tree_Selector( [ $el ], [], null, $this->index, $this->currentFileInfo ) ];
$sels[0]->mediaEmpty = true;
return $sels;
}
public function markReferenced() {
$this->rules[0]->markReferenced();
$this->isReferenced = true;
Less_Tree::ReferencedArray( $this->rules[0]->rules );
}
// evaltop
public function compileTop( $env ) {
$result = $this;
if ( count( $env->mediaBlocks ) > 1 ) {
$selectors = $this->emptySelectors();
$result = new Less_Tree_Ruleset( $selectors, $env->mediaBlocks );
$result->multiMedia = true;
}
$env->mediaBlocks = [];
$env->mediaPath = [];
return $result;
}
/**
* @param Less_Environment $env
* @return Less_Tree_Ruleset
*/
public function compileNested( $env ) {
$path = array_merge( $env->mediaPath, [ $this ] );
'@phan-var self[] $path';
// Extract the media-query conditions separated with `,` (OR).
foreach ( $path as $key => $p ) {
$value = $p->features instanceof Less_Tree_Value ? $p->features->value : $p->features;
$path[$key] = is_array( $value ) ? $value : [ $value ];
}
'@phan-var array<array<Less_Tree>> $path';
// Trace all permutations to generate the resulting media-query.
//
// (a, b and c) with nested (d, e) ->
// a and d
// a and e
// b and c and d
// b and c and e
$permuted = $this->permute( $path );
'@phan-var (Less_Tree|string)[][] $permuted';
$expressions = [];
foreach ( $permuted as $path ) {
for ( $i = 0, $len = count( $path ); $i < $len; $i++ ) {
$path[$i] = Less_Parser::is_method( $path[$i], 'toCSS' ) ? $path[$i] : new Less_Tree_Anonymous( $path[$i] );
}
for ( $i = count( $path ) - 1; $i > 0; $i-- ) {
array_splice( $path, $i, 0, [ new Less_Tree_Anonymous( 'and' ) ] );
}
$expressions[] = new Less_Tree_Expression( $path );
}
$this->features = new Less_Tree_Value( $expressions );
// Fake a tree-node that doesn't output anything.
return new Less_Tree_Ruleset( [], [] );
}
public function permute( $arr ) {
if ( !$arr ) {
return [];
}
if ( count( $arr ) == 1 ) {
return $arr[0];
}
$result = [];
$rest = $this->permute( array_slice( $arr, 1 ) );
foreach ( $rest as $r ) {
foreach ( $arr[0] as $a ) {
$result[] = array_merge(
is_array( $a ) ? $a : [ $a ],
is_array( $r ) ? $r : [ $r ]
);
}
}
return $result;
}
public function bubbleSelectors( $selectors ) {
if ( !$selectors ) {
return;
}
$this->rules = [ new Less_Tree_Ruleset( $selectors, [ $this->rules[0] ] ) ];
}
}

View File

@ -0,0 +1,197 @@
<?php
/**
* @private
*/
class Less_Tree_Mixin_Call extends Less_Tree {
public $selector;
public $arguments;
public $index;
public $currentFileInfo;
public $important;
/**
* less.js: tree.mixin.Call
*
*/
public function __construct( $elements, $args, $index, $currentFileInfo, $important = false ) {
$this->selector = new Less_Tree_Selector( $elements );
$this->arguments = $args;
$this->index = $index;
$this->currentFileInfo = $currentFileInfo;
$this->important = $important;
}
// function accept($visitor){
// $this->selector = $visitor->visit($this->selector);
// $this->arguments = $visitor->visit($this->arguments);
//}
public function compile( $env ) {
$rules = [];
$match = false;
$isOneFound = false;
$candidates = [];
$defaultUsed = false;
$conditionResult = [];
$args = [];
foreach ( $this->arguments as $a ) {
$args[] = [ 'name' => $a['name'], 'value' => $a['value']->compile( $env ) ];
}
foreach ( $env->frames as $frame ) {
$mixins = $frame->find( $this->selector );
if ( !$mixins ) {
continue;
}
$isOneFound = true;
$defNone = 0;
$defTrue = 1;
$defFalse = 2;
// To make `default()` function independent of definition order we have two "subpasses" here.
// At first we evaluate each guard *twice* (with `default() == true` and `default() == false`),
// and build candidate list with corresponding flags. Then, when we know all possible matches,
// we make a final decision.
$mixins_len = count( $mixins );
for ( $m = 0; $m < $mixins_len; $m++ ) {
$mixin = $mixins[$m];
if ( $this->IsRecursive( $env, $mixin ) ) {
continue;
}
if ( $mixin->matchArgs( $args, $env ) ) {
$candidate = [ 'mixin' => $mixin, 'group' => $defNone ];
if ( $mixin instanceof Less_Tree_Ruleset ) {
for ( $f = 0; $f < 2; $f++ ) {
Less_Tree_DefaultFunc::value( $f );
$conditionResult[$f] = $mixin->matchCondition( $args, $env );
}
// PhanTypeInvalidDimOffset -- False positive
'@phan-var array{0:bool,1:bool} $conditionResult';
if ( $conditionResult[0] || $conditionResult[1] ) {
if ( $conditionResult[0] != $conditionResult[1] ) {
$candidate['group'] = $conditionResult[1] ? $defTrue : $defFalse;
}
$candidates[] = $candidate;
}
} else {
$candidates[] = $candidate;
}
$match = true;
}
}
Less_Tree_DefaultFunc::reset();
$count = [ 0, 0, 0 ];
for ( $m = 0; $m < count( $candidates ); $m++ ) {
$count[ $candidates[$m]['group'] ]++;
}
if ( $count[$defNone] > 0 ) {
$defaultResult = $defFalse;
} else {
$defaultResult = $defTrue;
if ( ( $count[$defTrue] + $count[$defFalse] ) > 1 ) {
throw new Exception( 'Ambiguous use of `default()` found when matching for `' . $this->format( $args ) . '`' );
}
}
$candidates_length = count( $candidates );
$length_1 = ( $candidates_length == 1 );
for ( $m = 0; $m < $candidates_length; $m++ ) {
$candidate = $candidates[$m]['group'];
if ( ( $candidate === $defNone ) || ( $candidate === $defaultResult ) ) {
try{
$mixin = $candidates[$m]['mixin'];
if ( !( $mixin instanceof Less_Tree_Mixin_Definition ) ) {
$mixin = new Less_Tree_Mixin_Definition( '', [], $mixin->rules, null, false );
$mixin->originalRuleset = $mixins[$m]->originalRuleset;
}
$rules = array_merge( $rules, $mixin->evalCall( $env, $args, $this->important )->rules );
} catch ( Exception $e ) {
// throw new Less_Exception_Compiler($e->getMessage(), $e->index, null, $this->currentFileInfo['filename']);
throw new Less_Exception_Compiler( $e->getMessage(), null, null, $this->currentFileInfo );
}
}
}
if ( $match ) {
if ( !$this->currentFileInfo || !isset( $this->currentFileInfo['reference'] ) || !$this->currentFileInfo['reference'] ) {
Less_Tree::ReferencedArray( $rules );
}
return $rules;
}
}
if ( $isOneFound ) {
$selectorName = $this->selector->toCSS();
throw new Less_Exception_Compiler( 'No matching definition was found for ' . $selectorName . ' with args `' . $this->Format( $args ) . '`', null, $this->index, $this->currentFileInfo );
} else {
throw new Less_Exception_Compiler( trim( $this->selector->toCSS() ) . " is undefined in " . $this->currentFileInfo['filename'], null, $this->index );
}
}
/**
* Format the args for use in exception messages
*
*/
private function Format( $args ) {
$message = [];
if ( $args ) {
foreach ( $args as $a ) {
$argValue = '';
if ( $a['name'] ) {
$argValue .= $a['name'] . ':';
}
if ( is_object( $a['value'] ) ) {
$argValue .= $a['value']->toCSS();
} else {
$argValue .= '???';
}
$message[] = $argValue;
}
}
return implode( ', ', $message );
}
/**
* Are we in a recursive mixin call?
*
* @return bool
*/
private function IsRecursive( $env, $mixin ) {
foreach ( $env->frames as $recur_frame ) {
if ( !( $mixin instanceof Less_Tree_Mixin_Definition ) ) {
if ( $mixin === $recur_frame ) {
return true;
}
if ( isset( $recur_frame->originalRuleset ) && $mixin->ruleset_id === $recur_frame->originalRuleset ) {
return true;
}
}
}
return false;
}
}

View File

@ -0,0 +1,234 @@
<?php
/**
* @private
*/
class Less_Tree_Mixin_Definition extends Less_Tree_Ruleset {
public $name;
public $selectors;
public $params;
public $arity = 0;
public $rules;
public $lookups = [];
public $required = 0;
public $frames = [];
public $condition;
public $variadic;
// less.js : /lib/less/tree/mixin.js : tree.mixin.Definition
public function __construct( $name, $params, $rules, $condition, $variadic = false, $frames = [] ) {
$this->name = $name;
$this->selectors = [ new Less_Tree_Selector( [ new Less_Tree_Element( null, $name ) ] ) ];
$this->params = $params;
$this->condition = $condition;
$this->variadic = $variadic;
$this->rules = $rules;
if ( $params ) {
$this->arity = count( $params );
foreach ( $params as $p ) {
if ( !isset( $p['name'] ) || ( $p['name'] && !isset( $p['value'] ) ) ) {
$this->required++;
}
}
}
$this->frames = $frames;
$this->SetRulesetIndex();
}
// function accept( $visitor ){
// $this->params = $visitor->visit($this->params);
// $this->rules = $visitor->visit($this->rules);
// $this->condition = $visitor->visit($this->condition);
//}
public function toCSS() {
return '';
}
// less.js : /lib/less/tree/mixin.js : tree.mixin.Definition.evalParams
public function compileParams( $env, $mixinFrames, $args = [], &$evaldArguments = [] ) {
$frame = new Less_Tree_Ruleset( null, [] );
$params = $this->params;
$mixinEnv = null;
$argsLength = 0;
if ( $args ) {
$argsLength = count( $args );
for ( $i = 0; $i < $argsLength; $i++ ) {
$arg = $args[$i];
if ( $arg && $arg['name'] ) {
$isNamedFound = false;
foreach ( $params as $j => $param ) {
if ( !isset( $evaldArguments[$j] ) && $arg['name'] === $param['name'] ) {
$evaldArguments[$j] = $arg['value']->compile( $env );
array_unshift( $frame->rules, new Less_Tree_Rule( $arg['name'], $arg['value']->compile( $env ) ) );
$isNamedFound = true;
break;
}
}
if ( !$isNamedFound ) {
throw new Less_Exception_Compiler( "Named argument for " . $this->name . ' ' . $args[$i]['name'] . ' not found' );
}
array_splice( $args, $i, 1 );
$i--;
$argsLength--;
}
}
}
$argIndex = 0;
foreach ( $params as $i => $param ) {
if ( isset( $evaldArguments[$i] ) ) {
continue;
}
$arg = null;
if ( isset( $args[$argIndex] ) ) {
$arg = $args[$argIndex];
}
if ( isset( $param['name'] ) && $param['name'] ) {
if ( isset( $param['variadic'] ) ) {
$varargs = [];
for ( $j = $argIndex; $j < $argsLength; $j++ ) {
$varargs[] = $args[$j]['value']->compile( $env );
}
$expression = new Less_Tree_Expression( $varargs );
array_unshift( $frame->rules, new Less_Tree_Rule( $param['name'], $expression->compile( $env ) ) );
} else {
$val = ( $arg && $arg['value'] ) ? $arg['value'] : false;
if ( $val ) {
$val = $val->compile( $env );
} elseif ( isset( $param['value'] ) ) {
if ( !$mixinEnv ) {
$mixinEnv = new Less_Environment();
$mixinEnv->frames = array_merge( [ $frame ], $mixinFrames );
}
$val = $param['value']->compile( $mixinEnv );
$frame->resetCache();
} else {
throw new Less_Exception_Compiler( "Wrong number of arguments for " . $this->name . " (" . $argsLength . ' for ' . $this->arity . ")" );
}
array_unshift( $frame->rules, new Less_Tree_Rule( $param['name'], $val ) );
$evaldArguments[$i] = $val;
}
}
if ( isset( $param['variadic'] ) && $args ) {
for ( $j = $argIndex; $j < $argsLength; $j++ ) {
$evaldArguments[$j] = $args[$j]['value']->compile( $env );
}
}
$argIndex++;
}
ksort( $evaldArguments );
$evaldArguments = array_values( $evaldArguments );
return $frame;
}
public function compile( $env ) {
if ( $this->frames ) {
return new self( $this->name, $this->params, $this->rules, $this->condition, $this->variadic, $this->frames );
}
return new self( $this->name, $this->params, $this->rules, $this->condition, $this->variadic, $env->frames );
}
public function evalCall( $env, $args = null, $important = null ) {
Less_Environment::$mixin_stack++;
$_arguments = [];
if ( $this->frames ) {
$mixinFrames = array_merge( $this->frames, $env->frames );
} else {
$mixinFrames = $env->frames;
}
$frame = $this->compileParams( $env, $mixinFrames, $args, $_arguments );
$ex = new Less_Tree_Expression( $_arguments );
array_unshift( $frame->rules, new Less_Tree_Rule( '@arguments', $ex->compile( $env ) ) );
$ruleset = new Less_Tree_Ruleset( null, $this->rules );
$ruleset->originalRuleset = $this->ruleset_id;
$ruleSetEnv = new Less_Environment();
$ruleSetEnv->frames = array_merge( [ $this, $frame ], $mixinFrames );
$ruleset = $ruleset->compile( $ruleSetEnv );
if ( $important ) {
$ruleset = $ruleset->makeImportant();
}
Less_Environment::$mixin_stack--;
return $ruleset;
}
/** @return bool */
public function matchCondition( $args, $env ) {
if ( !$this->condition ) {
return true;
}
// set array to prevent error on array_merge
if ( !is_array( $this->frames ) ) {
$this->frames = [];
}
$frame = $this->compileParams( $env, array_merge( $this->frames, $env->frames ), $args );
$compile_env = new Less_Environment();
$compile_env->frames = array_merge(
[ $frame ], // the parameter variables
$this->frames, // the parent namespace/mixin frames
$env->frames // the current environment frames
);
$compile_env->functions = $env->functions;
return (bool)$this->condition->compile( $compile_env );
}
public function matchArgs( $args, $env = null ) {
$argsLength = count( $args );
if ( !$this->variadic ) {
if ( $argsLength < $this->required ) {
return false;
}
if ( $argsLength > count( $this->params ) ) {
return false;
}
} else {
if ( $argsLength < ( $this->required - 1 ) ) {
return false;
}
}
$len = min( $argsLength, $this->arity );
for ( $i = 0; $i < $len; $i++ ) {
if ( !isset( $this->params[$i]['name'] ) && !isset( $this->params[$i]['variadic'] ) ) {
if ( $args[$i]['value']->compile( $env )->toCSS() != $this->params[$i]['value']->compile( $env )->toCSS() ) {
return false;
}
}
}
return true;
}
}

View File

@ -0,0 +1,48 @@
<?php
/**
* A simple CSS name-value pair, e.g. `width: 100px;`
*
* In bootstrap, there are about 600-1000 simple name-value pairs (depending on
* how forgiving the match is) -vs- 6,020 dynamic rules (Less_Tree_Rule).
*
* Using the name-value object can speed up bootstrap compilation slightly, but
* it breaks color keyword interpretation: `color: red` -> `color: #FF0000`.
*
* @private
*/
class Less_Tree_NameValue extends Less_Tree implements Less_Tree_HasValueProperty {
public $name;
public $value;
public $index;
public $currentFileInfo;
public $important = '';
public function __construct( $name, $value = null, $index = null, $currentFileInfo = null ) {
$this->name = $name;
$this->value = $value;
$this->index = $index;
$this->currentFileInfo = $currentFileInfo;
}
public function genCSS( $output ) {
$output->add(
$this->name
. Less_Environment::$_outputMap[': ']
. $this->value
. $this->important
. ( ( ( Less_Environment::$lastRule && Less_Parser::$options['compress'] ) ) ? "" : ";" ),
$this->currentFileInfo, $this->index );
}
public function compile( $env ) {
return $this;
}
public function makeImportant() {
$new = new self( $this->name, $this->value, $this->index, $this->currentFileInfo );
$new->important = ' !important';
return $new;
}
}

View File

@ -0,0 +1,32 @@
<?php
/**
* @private
*/
class Less_Tree_Negative extends Less_Tree implements Less_Tree_HasValueProperty {
public $value;
public function __construct( $node ) {
$this->value = $node;
}
// function accept($visitor) {
// $this->value = $visitor->visit($this->value);
//}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$output->add( '-' );
$this->value->genCSS( $output );
}
public function compile( $env ) {
if ( Less_Environment::isMathOn() ) {
$ret = new Less_Tree_Operation( '*', [ new Less_Tree_Dimension( -1 ), $this->value ] );
return $ret->compile( $env );
}
return new self( $this->value->compile( $env ) );
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* @private
*/
class Less_Tree_Operation extends Less_Tree {
public $op;
public $operands;
public $isSpaced;
/**
* @param string $op
*/
public function __construct( $op, $operands, $isSpaced = false ) {
$this->op = trim( $op );
$this->operands = $operands;
$this->isSpaced = $isSpaced;
}
public function accept( $visitor ) {
$this->operands = $visitor->visitArray( $this->operands );
}
public function compile( $env ) {
$a = $this->operands[0]->compile( $env );
$b = $this->operands[1]->compile( $env );
// Skip operation if argument was not compiled down to a non-operable value.
// For example, if one argument is a Less_Tree_Call like 'var(--foo)' then we
// preserve it as literal for native CSS.
// https://phabricator.wikimedia.org/T331688
if ( Less_Environment::isMathOn() ) {
if ( $a instanceof Less_Tree_Dimension && $b instanceof Less_Tree_Color ) {
$a = $a->toColor();
} elseif ( $b instanceof Less_Tree_Dimension && $a instanceof Less_Tree_Color ) {
$b = $b->toColor();
}
if ( !( $a instanceof Less_Tree_Dimension || $a instanceof Less_Tree_Color ) ) {
throw new Less_Exception_Compiler( "Operation on an invalid type" );
}
if ( $b instanceof Less_Tree_Dimension || $b instanceof Less_Tree_Color ) {
return $a->operate( $this->op, $b );
}
}
return new self( $this->op, [ $a, $b ], $this->isSpaced );
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$this->operands[0]->genCSS( $output );
if ( $this->isSpaced ) {
$output->add( " " );
}
$output->add( $this->op );
if ( $this->isSpaced ) {
$output->add( ' ' );
}
$this->operands[1]->genCSS( $output );
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* @private
*/
class Less_Tree_Paren extends Less_Tree implements Less_Tree_HasValueProperty {
/** @var Less_Tree */
public $value;
/**
* @param Less_Tree $value
*/
public function __construct( $value ) {
$this->value = $value;
}
public function accept( $visitor ) {
$this->value = $visitor->visitObj( $this->value );
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$output->add( '(' );
$this->value->genCSS( $output );
$output->add( ')' );
}
public function compile( $env ) {
return new self( $this->value->compile( $env ) );
}
}

View File

@ -0,0 +1,75 @@
<?php
/**
* @private
*/
class Less_Tree_Quoted extends Less_Tree implements Less_Tree_HasValueProperty {
public $escaped;
/** @var string */
public $value;
public $quote;
public $index;
public $currentFileInfo;
/**
* @param string $str
*/
public function __construct( $str, $content = '', $escaped = false, $index = false, $currentFileInfo = null ) {
$this->escaped = $escaped;
$this->value = $content;
if ( $str ) {
$this->quote = $str[0];
}
$this->index = $index;
$this->currentFileInfo = $currentFileInfo;
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
if ( !$this->escaped ) {
$output->add( $this->quote, $this->currentFileInfo, $this->index );
}
$output->add( $this->value );
if ( !$this->escaped ) {
$output->add( $this->quote );
}
}
public function compile( $env ) {
$value = $this->value;
if ( preg_match_all( '/`([^`]+)`/', $this->value, $matches ) ) {
foreach ( $matches[1] as $i => $match ) {
$js = new Less_Tree_JavaScript( $match, $this->index, true );
$js = $js->compile( $env )->value;
$value = str_replace( $matches[0][$i], $js, $value );
}
}
if ( preg_match_all( '/@\{([\w-]+)\}/', $value, $matches ) ) {
foreach ( $matches[1] as $i => $match ) {
$v = new Less_Tree_Variable( '@' . $match, $this->index, $this->currentFileInfo );
$v = $v->compile( $env );
$v = ( $v instanceof self ) ? $v->value : $v->toCSS();
$value = str_replace( $matches[0][$i], $v, $value );
}
}
return new self( $this->quote . $value . $this->quote, $value, $this->escaped, $this->index, $this->currentFileInfo );
}
public function compare( $x ) {
if ( !Less_Parser::is_method( $x, 'toCSS' ) ) {
return -1;
}
$left = $this->toCSS();
$right = $x->toCSS();
if ( $left === $right ) {
return 0;
}
return $left < $right ? -1 : 1;
}
}

View File

@ -0,0 +1,122 @@
<?php
/**
* @private
*/
class Less_Tree_Rule extends Less_Tree implements Less_Tree_HasValueProperty {
public $name;
/** @var Less_Tree */
public $value;
/** @var string */
public $important;
public $merge;
public $index;
public $inline;
public $variable;
public $currentFileInfo;
/**
* @param string|array<Less_Tree_Keyword|Less_Tree_Variable> $name
* @param mixed $value
* @param null|false|string $important
* @param null|false|string $merge
* @param int|null $index
* @param array|null $currentFileInfo
* @param bool $inline
*/
public function __construct( $name, $value = null, $important = null, $merge = null, $index = null, $currentFileInfo = null, $inline = false ) {
$this->name = $name;
$this->value = ( $value instanceof Less_Tree )
? $value
: new Less_Tree_Value( [ $value ] );
$this->important = $important ? ' ' . trim( $important ) : '';
$this->merge = $merge;
$this->index = $index;
$this->currentFileInfo = $currentFileInfo;
$this->inline = $inline;
$this->variable = ( is_string( $name ) && $name[0] === '@' );
}
public function accept( $visitor ) {
$this->value = $visitor->visitObj( $this->value );
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$output->add( $this->name . Less_Environment::$_outputMap[': '], $this->currentFileInfo, $this->index );
try{
$this->value->genCSS( $output );
}catch ( Less_Exception_Parser $e ) {
$e->index = $this->index;
$e->currentFile = $this->currentFileInfo;
throw $e;
}
$output->add( $this->important . ( ( $this->inline || ( Less_Environment::$lastRule && Less_Parser::$options['compress'] ) ) ? "" : ";" ), $this->currentFileInfo, $this->index );
}
/**
* @param Less_Environment $env
* @return self
*/
public function compile( $env ) {
$name = $this->name;
if ( is_array( $name ) ) {
// expand 'primitive' name directly to get
// things faster (~10% for benchmark.less):
if ( count( $name ) === 1 && $name[0] instanceof Less_Tree_Keyword ) {
$name = $name[0]->value;
} else {
$name = $this->CompileName( $env, $name );
}
}
$strictMathBypass = Less_Parser::$options['strictMath'];
if ( $name === "font" && !Less_Parser::$options['strictMath'] ) {
Less_Parser::$options['strictMath'] = true;
}
try {
$evaldValue = $this->value->compile( $env );
if ( !$this->variable && $evaldValue instanceof Less_Tree_DetachedRuleset ) {
throw new Less_Exception_Compiler( "Rulesets cannot be evaluated on a property.", null, $this->index, $this->currentFileInfo );
}
if ( Less_Environment::$mixin_stack ) {
$return = new self( $name, $evaldValue, $this->important, $this->merge, $this->index, $this->currentFileInfo, $this->inline );
} else {
$this->name = $name;
$this->value = $evaldValue;
$return = $this;
}
} catch ( Less_Exception_Parser $e ) {
if ( !is_numeric( $e->index ) ) {
$e->index = $this->index;
$e->currentFile = $this->currentFileInfo;
$e->genMessage();
}
throw $e;
}
Less_Parser::$options['strictMath'] = $strictMathBypass;
return $return;
}
public function CompileName( $env, $name ) {
$output = new Less_Output();
foreach ( $name as $n ) {
$n->compile( $env )->genCSS( $output );
}
return $output->toString();
}
public function makeImportant() {
return new self( $this->name, $this->value, '!important', $this->merge, $this->index, $this->currentFileInfo, $this->inline );
}
}

View File

@ -0,0 +1,729 @@
<?php
/**
* @private
*/
class Less_Tree_Ruleset extends Less_Tree {
protected $lookups;
public $_variables;
public $_rulesets;
public $strictImports;
public $selectors;
public $rules;
public $root;
public $allowImports;
public $paths;
public $firstRoot;
public $multiMedia;
public $allExtends;
/** @var int */
public $ruleset_id;
/** @var int */
public $originalRuleset;
public $first_oelements;
public function SetRulesetIndex() {
$this->ruleset_id = Less_Parser::$next_id++;
$this->originalRuleset = $this->ruleset_id;
if ( $this->selectors ) {
foreach ( $this->selectors as $sel ) {
if ( $sel->_oelements ) {
$this->first_oelements[$sel->_oelements[0]] = true;
}
}
}
}
/**
* @param null|Less_Tree_Selector[] $selectors
* @param Less_Tree[] $rules
* @param null|bool $strictImports
*/
public function __construct( $selectors, $rules, $strictImports = null ) {
$this->selectors = $selectors;
$this->rules = $rules;
$this->lookups = [];
$this->strictImports = $strictImports;
$this->SetRulesetIndex();
}
public function accept( $visitor ) {
if ( $this->paths !== null ) {
$paths_len = count( $this->paths );
for ( $i = 0; $i < $paths_len; $i++ ) {
$this->paths[$i] = $visitor->visitArray( $this->paths[$i] );
}
} elseif ( $this->selectors ) {
$this->selectors = $visitor->visitArray( $this->selectors );
}
if ( $this->rules ) {
$this->rules = $visitor->visitArray( $this->rules );
}
}
/**
* @param Less_Environment $env
* @return self
* @see less-2.5.3.js#Ruleset.prototype.eval
*/
public function compile( $env ) {
$ruleset = $this->PrepareRuleset( $env );
// Store the frames around mixin definitions,
// so they can be evaluated like closures when the time comes.
$rsRuleCnt = count( $ruleset->rules );
for ( $i = 0; $i < $rsRuleCnt; $i++ ) {
// These checks are the equivalent of the rule.evalFirst property in less.js
if ( $ruleset->rules[$i] instanceof Less_Tree_Mixin_Definition || $ruleset->rules[$i] instanceof Less_Tree_DetachedRuleset ) {
$ruleset->rules[$i] = $ruleset->rules[$i]->compile( $env );
}
}
$mediaBlockCount = count( $env->mediaBlocks );
// Evaluate mixin calls.
$this->EvalMixinCalls( $ruleset, $env, $rsRuleCnt );
// Evaluate everything else
for ( $i = 0; $i < $rsRuleCnt; $i++ ) {
if ( !( $ruleset->rules[$i] instanceof Less_Tree_Mixin_Definition || $ruleset->rules[$i] instanceof Less_Tree_DetachedRuleset ) ) {
$ruleset->rules[$i] = $ruleset->rules[$i]->compile( $env );
}
}
// Evaluate everything else
for ( $i = 0; $i < $rsRuleCnt; $i++ ) {
$rule = $ruleset->rules[$i];
// for rulesets, check if it is a css guard and can be removed
if ( $rule instanceof self && $rule->selectors && count( $rule->selectors ) === 1 ) {
// check if it can be folded in (e.g. & where)
if ( $rule->selectors[0]->isJustParentSelector() ) {
array_splice( $ruleset->rules, $i--, 1 );
$rsRuleCnt--;
for ( $j = 0; $j < count( $rule->rules ); $j++ ) {
$subRule = $rule->rules[$j];
if ( !( $subRule instanceof Less_Tree_Rule ) || !$subRule->variable ) {
array_splice( $ruleset->rules, ++$i, 0, [ $subRule ] );
$rsRuleCnt++;
}
}
}
}
}
// Pop the stack
$env->shiftFrame();
if ( $mediaBlockCount ) {
$len = count( $env->mediaBlocks );
for ( $i = $mediaBlockCount; $i < $len; $i++ ) {
$env->mediaBlocks[$i]->bubbleSelectors( $ruleset->selectors );
}
}
return $ruleset;
}
/**
* Compile Less_Tree_Mixin_Call objects
*
* @param self $ruleset
* @param Less_Environment $env
* @param int &$rsRuleCnt
*/
private function EvalMixinCalls( $ruleset, $env, &$rsRuleCnt ) {
for ( $i = 0; $i < $rsRuleCnt; $i++ ) {
$rule = $ruleset->rules[$i];
if ( $rule instanceof Less_Tree_Mixin_Call ) {
$rule = $rule->compile( $env );
$temp = [];
foreach ( $rule as $r ) {
if ( ( $r instanceof Less_Tree_Rule ) && $r->variable ) {
// do not pollute the scope if the variable is
// already there. consider returning false here
// but we need a way to "return" variable from mixins
if ( !$ruleset->variable( $r->name ) ) {
$temp[] = $r;
}
} else {
$temp[] = $r;
}
}
$temp_count = count( $temp ) - 1;
array_splice( $ruleset->rules, $i, 1, $temp );
$rsRuleCnt += $temp_count;
$i += $temp_count;
$ruleset->resetCache();
} elseif ( $rule instanceof Less_Tree_RulesetCall ) {
$rule = $rule->compile( $env );
$rules = [];
foreach ( $rule->rules as $r ) {
if ( ( $r instanceof Less_Tree_Rule ) && $r->variable ) {
continue;
}
$rules[] = $r;
}
array_splice( $ruleset->rules, $i, 1, $rules );
$temp_count = count( $rules );
$rsRuleCnt += $temp_count - 1;
$i += $temp_count - 1;
$ruleset->resetCache();
}
}
}
/**
* Compile the selectors and create a new ruleset object for the compile() method
*
* @param Less_Environment $env
* @return self
*/
private function PrepareRuleset( $env ) {
// NOTE: Preserve distinction between null and empty array when compiling
// $this->selectors to $selectors
$thisSelectors = $this->selectors;
$selectors = null;
$hasOnePassingSelector = false;
if ( $thisSelectors ) {
Less_Tree_DefaultFunc::error( "it is currently only allowed in parametric mixin guards," );
$selectors = [];
foreach ( $thisSelectors as $s ) {
$selector = $s->compile( $env );
$selectors[] = $selector;
if ( $selector->evaldCondition ) {
$hasOnePassingSelector = true;
}
}
Less_Tree_DefaultFunc::reset();
} else {
$hasOnePassingSelector = true;
}
if ( $this->rules && $hasOnePassingSelector ) {
// Copy the array (no need for slice in PHP)
$rules = $this->rules;
} else {
$rules = [];
}
$ruleset = new self( $selectors, $rules, $this->strictImports );
$ruleset->originalRuleset = $this->ruleset_id;
$ruleset->root = $this->root;
$ruleset->firstRoot = $this->firstRoot;
$ruleset->allowImports = $this->allowImports;
// push the current ruleset to the frames stack
$env->unshiftFrame( $ruleset );
// Evaluate imports
if ( $ruleset->root || $ruleset->allowImports || !$ruleset->strictImports ) {
$ruleset->evalImports( $env );
}
return $ruleset;
}
public function evalImports( $env ) {
$rules_len = count( $this->rules );
for ( $i = 0; $i < $rules_len; $i++ ) {
$rule = $this->rules[$i];
if ( $rule instanceof Less_Tree_Import ) {
$rules = $rule->compile( $env );
if ( is_array( $rules ) ) {
array_splice( $this->rules, $i, 1, $rules );
$temp_count = count( $rules ) - 1;
$i += $temp_count;
$rules_len += $temp_count;
} else {
array_splice( $this->rules, $i, 1, [ $rules ] );
}
$this->resetCache();
}
}
}
public function makeImportant() {
$important_rules = [];
foreach ( $this->rules as $rule ) {
if ( $rule instanceof Less_Tree_Rule || $rule instanceof self || $rule instanceof Less_Tree_NameValue ) {
$important_rules[] = $rule->makeImportant();
} else {
$important_rules[] = $rule;
}
}
return new self( $this->selectors, $important_rules, $this->strictImports );
}
public function matchArgs( $args, $env = null ) {
return !$args;
}
// lets you call a css selector with a guard
public function matchCondition( $args, $env ) {
$lastSelector = end( $this->selectors );
if ( !$lastSelector->evaldCondition ) {
return false;
}
if ( $lastSelector->condition && !$lastSelector->condition->compile( $env->copyEvalEnv( $env->frames ) ) ) {
return false;
}
return true;
}
public function resetCache() {
$this->_rulesets = null;
$this->_variables = null;
$this->lookups = [];
}
public function variables() {
$this->_variables = [];
foreach ( $this->rules as $r ) {
if ( $r instanceof Less_Tree_Rule && $r->variable === true ) {
$this->_variables[$r->name] = $r;
}
}
}
/**
* @param string $name
* @return Less_Tree_Rule|null
*/
public function variable( $name ) {
if ( $this->_variables === null ) {
$this->variables();
}
return $this->_variables[$name] ?? null;
}
public function find( $selector, $self = null ) {
$key = implode( ' ', $selector->_oelements );
if ( !isset( $this->lookups[$key] ) ) {
if ( !$self ) {
$self = $this->ruleset_id;
}
$this->lookups[$key] = [];
$first_oelement = $selector->_oelements[0];
foreach ( $this->rules as $rule ) {
if ( $rule instanceof self && $rule->ruleset_id != $self ) {
if ( isset( $rule->first_oelements[$first_oelement] ) ) {
foreach ( $rule->selectors as $ruleSelector ) {
$match = $selector->match( $ruleSelector );
if ( $match ) {
if ( $selector->elements_len > $match ) {
$this->lookups[$key] = array_merge( $this->lookups[$key], $rule->find( new Less_Tree_Selector( array_slice( $selector->elements, $match ) ), $self ) );
} else {
$this->lookups[$key][] = $rule;
}
break;
}
}
}
}
}
}
return $this->lookups[$key];
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
if ( !$this->root ) {
Less_Environment::$tabLevel++;
}
$tabRuleStr = $tabSetStr = '';
if ( !Less_Parser::$options['compress'] ) {
if ( Less_Environment::$tabLevel ) {
$tabRuleStr = "\n" . str_repeat( Less_Parser::$options['indentation'], Less_Environment::$tabLevel );
$tabSetStr = "\n" . str_repeat( Less_Parser::$options['indentation'], Less_Environment::$tabLevel - 1 );
} else {
$tabSetStr = $tabRuleStr = "\n";
}
}
$ruleNodes = [];
$rulesetNodes = [];
foreach ( $this->rules as $rule ) {
if ( $rule instanceof Less_Tree_Media ||
$rule instanceof Less_Tree_Directive ||
( $this->root && $rule instanceof Less_Tree_Comment ) ||
( $rule instanceof self && $rule->rules )
) {
$rulesetNodes[] = $rule;
} else {
$ruleNodes[] = $rule;
}
}
// If this is the root node, we don't render
// a selector, or {}.
if ( !$this->root ) {
$paths_len = count( $this->paths );
for ( $i = 0; $i < $paths_len; $i++ ) {
$path = $this->paths[$i];
$firstSelector = true;
foreach ( $path as $p ) {
$p->genCSS( $output, $firstSelector );
$firstSelector = false;
}
if ( $i + 1 < $paths_len ) {
$output->add( ',' . $tabSetStr );
}
}
$output->add( ( Less_Parser::$options['compress'] ? '{' : " {" ) . $tabRuleStr );
}
// Compile rules and rulesets
$ruleNodes_len = count( $ruleNodes );
$rulesetNodes_len = count( $rulesetNodes );
for ( $i = 0; $i < $ruleNodes_len; $i++ ) {
$rule = $ruleNodes[$i];
// @page{ directive ends up with root elements inside it, a mix of rules and rulesets
// In this instance we do not know whether it is the last property
if ( $i + 1 === $ruleNodes_len && ( !$this->root || $rulesetNodes_len === 0 || $this->firstRoot ) ) {
Less_Environment::$lastRule = true;
}
$rule->genCSS( $output );
if ( !Less_Environment::$lastRule ) {
$output->add( $tabRuleStr );
} else {
Less_Environment::$lastRule = false;
}
}
if ( !$this->root ) {
$output->add( $tabSetStr . '}' );
Less_Environment::$tabLevel--;
}
$firstRuleset = true;
$space = ( $this->root ? $tabRuleStr : $tabSetStr );
for ( $i = 0; $i < $rulesetNodes_len; $i++ ) {
if ( $ruleNodes_len && $firstRuleset ) {
$output->add( $space );
} elseif ( !$firstRuleset ) {
$output->add( $space );
}
$firstRuleset = false;
$rulesetNodes[$i]->genCSS( $output );
}
if ( !Less_Parser::$options['compress'] && $this->firstRoot ) {
$output->add( "\n" );
}
}
public function markReferenced() {
if ( !$this->selectors ) {
return;
}
foreach ( $this->selectors as $selector ) {
$selector->markReferenced();
}
}
/**
* @param Less_Tree_Selector[][] $context
* @param Less_Tree_Selector[]|null $selectors
* @return Less_Tree_Selector[][]
*/
public function joinSelectors( $context, $selectors ) {
$paths = [];
if ( $selectors !== null ) {
foreach ( $selectors as $selector ) {
$this->joinSelector( $paths, $context, $selector );
}
}
return $paths;
}
public function joinSelector( array &$paths, array $context, Less_Tree_Selector $selector ) {
$newPaths = [];
$hadParentSelector = $this->replaceParentSelector( $newPaths, $context, $selector );
if ( !$hadParentSelector ) {
if ( $context ) {
$newPaths = [];
foreach ( $context as $path ) {
$newPaths[] = array_merge( $path, [ $selector ] );
}
} else {
$newPaths = [ [ $selector ] ];
}
}
foreach ( $newPaths as $newPath ) {
$paths[] = $newPath;
}
}
/**
* Replace all parent selectors inside $inSelector with $context.
*
* @param array &$paths Resulting selectors are appended to $paths.
* @param mixed $context
* @param Less_Tree_Selector $inSelector Inner selector from Less_Tree_Paren
* @return bool True if $inSelector contained at least one parent selector
*/
private function replaceParentSelector( array &$paths, $context, Less_Tree_Selector $inSelector ) {
$hadParentSelector = false;
// The paths are [[Selector]]
// The first list is a list of comma separated selectors
// The inner list is a list of inheritance separated selectors
// e.g.
// .a, .b {
// .c {
// }
// }
// == [[.a] [.c]] [[.b] [.c]]
//
// the elements from the current selector so far
$currentElements = [];
// the current list of new selectors to add to the path.
// We will build it up. We initiate it with one empty selector as we "multiply" the new selectors
// by the parents
$newSelectors = [
[]
];
foreach ( $inSelector->elements as $el ) {
// non-parent reference elements just get added
if ( $el->value !== '&' ) {
$nestedSelector = $this->findNestedSelector( $el );
if ( $nestedSelector !== null ) {
$this->mergeElementsOnToSelectors( $currentElements, $newSelectors );
$nestedPaths = [];
$replacedNewSelectors = [];
$replaced = $this->replaceParentSelector( $nestedPaths, $context, $nestedSelector );
$hadParentSelector = $hadParentSelector || $replaced;
// $nestedPaths is populated by replaceParentSelector()
// $nestedPaths should have exactly one TODO, replaceParentSelector does not multiply selectors
foreach ( $nestedPaths as $nestedPath ) {
$replacementSelector = $this->createSelector( $nestedPath, $el );
// join selector path from $newSelectors with every selector path in $addPaths array.
// $el contains the element that is being replaced by $addPaths
//
// @see less-2.5.3.js#Ruleset-addAllReplacementsIntoPath
$addPaths = [ $replacementSelector ];
foreach ( $newSelectors as $newSelector ) {
$replacedNewSelectors[] = $this->addReplacementIntoPath( $newSelector, $addPaths, $el, $inSelector );
}
}
$newSelectors = $replacedNewSelectors;
$currentElements = [];
} else {
$currentElements[] = $el;
}
} else {
$hadParentSelector = true;
// the new list of selectors to add
$selectorsMultiplied = [];
// merge the current list of non parent selector elements
// on to the current list of selectors to add
$this->mergeElementsOnToSelectors( $currentElements, $newSelectors );
foreach ( $newSelectors as $sel ) {
// if we don't have any parent paths, the & might be in a mixin so that it can be used
// whether there are parents or not
if ( !$context ) {
// the combinator used on el should now be applied to the next element instead so that
// it is not lost
if ( $sel ) {
$sel[0]->elements[] = new Less_Tree_Element( $el->combinator, '', $el->index, $el->currentFileInfo );
}
$selectorsMultiplied[] = $sel;
} else {
// and the parent selectors
foreach ( $context as $parentSel ) {
// We need to put the current selectors
// then join the last selector's elements on to the parents selectors
$newSelectorPath = $this->addReplacementIntoPath( $sel, $parentSel, $el, $inSelector );
// add that to our new set of selectors
$selectorsMultiplied[] = $newSelectorPath;
}
}
}
// our new selectors has been multiplied, so reset the state
$newSelectors = $selectorsMultiplied;
$currentElements = [];
}
}
// if we have any elements left over (e.g. .a& .b == .b)
// add them on to all the current selectors
$this->mergeElementsOnToSelectors( $currentElements, $newSelectors );
foreach ( $newSelectors as &$sel ) {
$length = count( $sel );
if ( $length ) {
$paths[] = $sel;
$lastSelector = $sel[$length - 1];
$sel[$length - 1] = $lastSelector->createDerived( $lastSelector->elements, $inSelector->extendList );
}
}
return $hadParentSelector;
}
/**
* @param array $elementsToPak
* @param Less_Tree_Element $originalElement
* @return Less_Tree_Selector
*/
private function createSelector( array $elementsToPak, $originalElement ) {
if ( !$elementsToPak ) {
// This is an invalid call. Kept to match less.js. Appears unreachable.
// @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal
$containedElement = new Less_Tree_Paren( null );
} else {
$insideParent = [];
foreach ( $elementsToPak as $elToPak ) {
$insideParent[] = new Less_Tree_Element( null, $elToPak, $originalElement->index, $originalElement->currentFileInfo );
}
$containedElement = new Less_Tree_Paren( new Less_Tree_Selector( $insideParent ) );
}
$element = new Less_Tree_Element( null, $containedElement, $originalElement->index, $originalElement->currentFileInfo );
return new Less_Tree_Selector( [ $element ] );
}
/**
* @param Less_Tree_Element $element
* @return Less_Tree_Selector|null
*/
private function findNestedSelector( $element ) {
$maybeParen = $element->value;
if ( !( $maybeParen instanceof Less_Tree_Paren ) ) {
return null;
}
$maybeSelector = $maybeParen->value;
if ( !( $maybeSelector instanceof Less_Tree_Selector ) ) {
return null;
}
return $maybeSelector;
}
/**
* joins selector path from $beginningPath with selector path in $addPath.
*
* $replacedElement contains the element that is being replaced by $addPath
*
* @param Less_Tree_Selector[] $beginningPath
* @param Less_Tree_Selector[] $addPath
* @param Less_Tree_Element $replacedElement
* @param Less_Tree_Selector $originalSelector
* @return Less_Tree_Selector[] Concatenated path
* @see less-2.5.3.js#Ruleset-addReplacementIntoPath
*/
private function addReplacementIntoPath( array $beginningPath, array $addPath, $replacedElement, $originalSelector ) {
// our new selector path
$newSelectorPath = [];
// construct the joined selector - if `&` is the first thing this will be empty,
// if not newJoinedSelector will be the last set of elements in the selector
if ( $beginningPath ) {
// NOTE: less.js uses Array slice() to copy. In PHP, arrays are naturally copied by value.
$newSelectorPath = $beginningPath;
$lastSelector = array_pop( $newSelectorPath );
$newJoinedSelector = $originalSelector->createDerived( $lastSelector->elements );
} else {
$newJoinedSelector = $originalSelector->createDerived( [] );
}
if ( $addPath ) {
// if the & does not have a combinator that is "" or " " then
// and there is a combinator on the parent, then grab that.
// this also allows `+ a { & .b { .a & { ...`
$combinator = $replacedElement->combinator;
$parentEl = $addPath[0]->elements[0];
if ( $replacedElement->combinatorIsEmptyOrWhitespace && !$parentEl->combinatorIsEmptyOrWhitespace ) {
$combinator = $parentEl->combinator;
}
// join the elements so far with the first part of the parent
$newJoinedSelector->elements[] = new Less_Tree_Element( $combinator, $parentEl->value, $replacedElement->index, $replacedElement->currentFileInfo );
$newJoinedSelector->elements = array_merge(
$newJoinedSelector->elements,
array_slice( $addPath[0]->elements, 1 )
);
}
// now add the joined selector - but only if it is not empty
if ( $newJoinedSelector->elements ) {
$newSelectorPath[] = $newJoinedSelector;
}
// put together the parent selectors after the join (e.g. the rest of the parent)
if ( count( $addPath ) > 1 ) {
$newSelectorPath = array_merge( $newSelectorPath, array_slice( $addPath, 1 ) );
}
return $newSelectorPath;
}
public function mergeElementsOnToSelectors( $elements, &$selectors ) {
if ( !$elements ) {
return;
}
if ( !$selectors ) {
$selectors[] = [ new Less_Tree_Selector( $elements ) ];
return;
}
foreach ( $selectors as &$sel ) {
// if the previous thing in sel is a parent this needs to join on to it
if ( $sel ) {
$last = count( $sel ) - 1;
$sel[$last] = $sel[$last]->createDerived( array_merge( $sel[$last]->elements, $elements ) );
} else {
$sel[] = new Less_Tree_Selector( $elements );
}
}
}
}

View File

@ -0,0 +1,26 @@
<?php
/**
* @private
*/
class Less_Tree_RulesetCall extends Less_Tree {
public $variable;
public $type = "RulesetCall";
/**
* @param string $variable
*/
public function __construct( $variable ) {
$this->variable = $variable;
}
public function accept( $visitor ) {
}
public function compile( $env ) {
$variable = new Less_Tree_Variable( $this->variable );
$detachedRuleset = $variable->compile( $env );
'@phan-var Less_Tree_DetachedRuleset $detachedRuleset';
return $detachedRuleset->callEval( $env );
}
}

View File

@ -0,0 +1,175 @@
<?php
/**
* @private
*/
class Less_Tree_Selector extends Less_Tree {
public $elements;
public $condition;
public $extendList = [];
public $_css;
public $index;
public $evaldCondition = false;
public $currentFileInfo = [];
public $isReferenced;
public $mediaEmpty;
public $elements_len = 0;
public $_oelements;
public $_oelements_assoc;
public $_oelements_len;
public $cacheable = true;
/**
* @param Less_Tree_Element[] $elements
* @param Less_Tree_Element[] $extendList
* @param Less_Tree_Condition|null $condition
* @param int|null $index
* @param array|null $currentFileInfo
* @param bool|null $isReferenced
*/
public function __construct( $elements, $extendList = [], $condition = null, $index = null, $currentFileInfo = null, $isReferenced = null ) {
$this->elements = $elements;
$this->elements_len = count( $elements );
$this->extendList = $extendList;
$this->condition = $condition;
if ( $currentFileInfo ) {
$this->currentFileInfo = $currentFileInfo;
}
$this->isReferenced = $isReferenced;
if ( !$condition ) {
$this->evaldCondition = true;
}
$this->CacheElements();
}
public function accept( $visitor ) {
$this->elements = $visitor->visitArray( $this->elements );
$this->extendList = $visitor->visitArray( $this->extendList );
if ( $this->condition ) {
$this->condition = $visitor->visitObj( $this->condition );
}
if ( $visitor instanceof Less_Visitor_extendFinder ) {
$this->CacheElements();
}
}
public function createDerived( $elements, $extendList = null, $evaldCondition = null ) {
$newSelector = new self(
$elements,
( $extendList ?: $this->extendList ),
null,
$this->index,
$this->currentFileInfo,
$this->isReferenced
);
$newSelector->evaldCondition = $evaldCondition ?: $this->evaldCondition;
$newSelector->mediaEmpty = $this->mediaEmpty;
return $newSelector;
}
public function match( $other ) {
if ( !$other->_oelements || ( $this->elements_len < $other->_oelements_len ) ) {
return 0;
}
for ( $i = 0; $i < $other->_oelements_len; $i++ ) {
if ( $this->elements[$i]->value !== $other->_oelements[$i] ) {
return 0;
}
}
return $other->_oelements_len; // return number of matched elements
}
public function CacheElements() {
$this->_oelements = [];
$this->_oelements_assoc = [];
$css = '';
foreach ( $this->elements as $v ) {
$css .= $v->combinator;
if ( !$v->value_is_object ) {
$css .= $v->value;
continue;
}
if (
( $v->value instanceof Less_Tree_Selector || $v->value instanceof Less_Tree_Variable )
|| !is_string( $v->value->value ) ) {
$this->cacheable = false;
return;
}
$css .= $v->value->value;
}
$this->_oelements_len = preg_match_all( '/[,&#\.\w-](?:[\w-]|(?:\\\\.))*/', $css, $matches );
if ( $this->_oelements_len ) {
$this->_oelements = $matches[0];
if ( $this->_oelements[0] === '&' ) {
array_shift( $this->_oelements );
$this->_oelements_len--;
}
$this->_oelements_assoc = array_fill_keys( $this->_oelements, true );
}
}
public function isJustParentSelector() {
return !$this->mediaEmpty &&
count( $this->elements ) === 1 &&
$this->elements[0]->value === '&' &&
( $this->elements[0]->combinator === ' ' || $this->elements[0]->combinator === '' );
}
public function compile( $env ) {
$elements = [];
foreach ( $this->elements as $el ) {
$elements[] = $el->compile( $env );
}
$extendList = [];
foreach ( $this->extendList as $el ) {
$extendList[] = $el->compile( $el );
}
$evaldCondition = false;
if ( $this->condition ) {
$evaldCondition = $this->condition->compile( $env );
}
return $this->createDerived( $elements, $extendList, $evaldCondition );
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output, $firstSelector = true ) {
if ( !$firstSelector && $this->elements[0]->combinator === "" ) {
$output->add( ' ', $this->currentFileInfo, $this->index );
}
foreach ( $this->elements as $element ) {
$element->genCSS( $output );
}
}
public function markReferenced() {
$this->isReferenced = true;
}
public function getIsReferenced() {
return !isset( $this->currentFileInfo['reference'] ) || !$this->currentFileInfo['reference'] || $this->isReferenced;
}
public function getIsOutput() {
return $this->evaldCondition;
}
}

View File

@ -0,0 +1,19 @@
<?php
/**
* @private
*/
class Less_Tree_UnicodeDescriptor extends Less_Tree implements Less_Tree_HasValueProperty {
public $value;
public function __construct( $value ) {
$this->value = $value;
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$output->add( $this->value );
}
}

View File

@ -0,0 +1,136 @@
<?php
/**
* @private
*/
class Less_Tree_Unit extends Less_Tree {
public $numerator = [];
public $denominator = [];
public $backupUnit;
public function __construct( $numerator = [], $denominator = [], $backupUnit = null ) {
$this->numerator = $numerator;
$this->denominator = $denominator;
$this->backupUnit = $backupUnit;
}
public function __clone() {
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
if ( $this->numerator ) {
$output->add( $this->numerator[0] );
} elseif ( $this->denominator ) {
$output->add( $this->denominator[0] );
} elseif ( !Less_Parser::$options['strictUnits'] && $this->backupUnit ) {
$output->add( $this->backupUnit );
}
}
public function toString() {
$returnStr = implode( '*', $this->numerator );
foreach ( $this->denominator as $d ) {
$returnStr .= '/' . $d;
}
return $returnStr;
}
public function __toString() {
return $this->toString();
}
/**
* @param self $other
*/
public function compare( $other ) {
return $this->is( $other->toString() ) ? 0 : -1;
}
public function is( $unitString ) {
return $this->toString() === $unitString;
}
public function isLength() {
$css = $this->toCSS();
return (bool)preg_match( '/px|em|%|in|cm|mm|pc|pt|ex/', $css );
}
public function isAngle() {
return isset( Less_Tree_UnitConversions::$angle[$this->toCSS()] );
}
public function isEmpty() {
return !$this->numerator && !$this->denominator;
}
public function isSingular() {
return count( $this->numerator ) <= 1 && !$this->denominator;
}
public function usedUnits() {
$result = [];
foreach ( Less_Tree_UnitConversions::$groups as $groupName ) {
$group = Less_Tree_UnitConversions::${$groupName};
foreach ( $this->numerator as $atomicUnit ) {
if ( isset( $group[$atomicUnit] ) && !isset( $result[$groupName] ) ) {
$result[$groupName] = $atomicUnit;
}
}
foreach ( $this->denominator as $atomicUnit ) {
if ( isset( $group[$atomicUnit] ) && !isset( $result[$groupName] ) ) {
$result[$groupName] = $atomicUnit;
}
}
}
return $result;
}
public function cancel() {
$counter = [];
$backup = null;
foreach ( $this->numerator as $atomicUnit ) {
if ( !$backup ) {
$backup = $atomicUnit;
}
$counter[$atomicUnit] = ( $counter[$atomicUnit] ?? 0 ) + 1;
}
foreach ( $this->denominator as $atomicUnit ) {
if ( !$backup ) {
$backup = $atomicUnit;
}
$counter[$atomicUnit] = ( $counter[$atomicUnit] ?? 0 ) - 1;
}
$this->numerator = [];
$this->denominator = [];
foreach ( $counter as $atomicUnit => $count ) {
if ( $count > 0 ) {
for ( $i = 0; $i < $count; $i++ ) {
$this->numerator[] = $atomicUnit;
}
} elseif ( $count < 0 ) {
for ( $i = 0; $i < -$count; $i++ ) {
$this->denominator[] = $atomicUnit;
}
}
}
if ( !$this->numerator && !$this->denominator && $backup ) {
$this->backupUnit = $backup;
}
sort( $this->numerator );
sort( $this->denominator );
}
}

View File

@ -0,0 +1,31 @@
<?php
/**
* @private
*/
class Less_Tree_UnitConversions {
public static $groups = [ 'length','duration','angle' ];
public static $length = [
'm' => 1,
'cm' => 0.01,
'mm' => 0.001,
'in' => 0.0254,
'px' => 0.000264583, // 0.0254 / 96,
'pt' => 0.000352778, // 0.0254 / 72,
'pc' => 0.004233333, // 0.0254 / 72 * 12
];
public static $duration = [
's' => 1,
'ms' => 0.001
];
public static $angle = [
'rad' => 0.1591549430919, // 1/(2*M_PI),
'deg' => 0.002777778, // 1/360,
'grad' => 0.0025, // 1/400,
'turn' => 1
];
}

View File

@ -0,0 +1,76 @@
<?php
/**
* @private
*/
class Less_Tree_Url extends Less_Tree implements Less_Tree_HasValueProperty {
public $attrs;
public $value;
public $currentFileInfo;
public $isEvald;
/**
* @param Less_Tree_Variable|Less_Tree_Quoted|Less_Tree_Anonymous $value
* @param array|null $currentFileInfo
* @param bool|null $isEvald
*/
public function __construct( Less_Tree $value, $currentFileInfo = null, $isEvald = null ) {
$this->value = $value;
$this->currentFileInfo = $currentFileInfo;
$this->isEvald = $isEvald;
}
public function accept( $visitor ) {
$this->value = $visitor->visitObj( $this->value );
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$output->add( 'url(' );
$this->value->genCSS( $output );
$output->add( ')' );
}
/**
* @param Less_Environment $env
*/
public function compile( $env ) {
$val = $this->value->compile( $env );
if ( !$this->isEvald ) {
// Add the base path if the URL is relative
if ( Less_Parser::$options['relativeUrls']
&& $this->currentFileInfo
&& is_string( $val->value )
&& Less_Environment::isPathRelative( $val->value )
) {
$rootpath = $this->currentFileInfo['uri_root'];
if ( !$val->quote ) {
$rootpath = preg_replace( '/[\(\)\'"\s]/', '\\$1', $rootpath );
}
$val->value = $rootpath . $val->value;
}
$val->value = Less_Environment::normalizePath( $val->value );
}
// Add cache buster if enabled
if ( Less_Parser::$options['urlArgs'] ) {
if ( !preg_match( '/^\s*data:/', $val->value ) ) {
$delimiter = strpos( $val->value, '?' ) === false ? '?' : '&';
$urlArgs = $delimiter . Less_Parser::$options['urlArgs'];
$hash_pos = strpos( $val->value, '#' );
if ( $hash_pos !== false ) {
$val->value = substr_replace( $val->value, $urlArgs, $hash_pos, 0 );
} else {
$val->value .= $urlArgs;
}
}
}
return new self( $val, $this->currentFileInfo, true );
}
}

View File

@ -0,0 +1,46 @@
<?php
/**
* @private
*/
class Less_Tree_Value extends Less_Tree implements Less_Tree_HasValueProperty {
/** @var Less_Tree[] */
public $value;
/**
* @param array<Less_Tree> $value
*/
public function __construct( $value ) {
$this->value = $value;
}
public function accept( $visitor ) {
$this->value = $visitor->visitArray( $this->value );
}
public function compile( $env ) {
$ret = [];
$i = 0;
foreach ( $this->value as $i => $v ) {
$ret[] = $v->compile( $env );
}
if ( $i > 0 ) {
return new self( $ret );
}
return $ret[0];
}
/**
* @see Less_Tree::genCSS
*/
public function genCSS( $output ) {
$len = count( $this->value );
for ( $i = 0; $i < $len; $i++ ) {
$this->value[$i]->genCSS( $output );
if ( $i + 1 < $len ) {
$output->add( Less_Environment::$_outputMap[','] );
}
}
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* @private
*/
class Less_Tree_Variable extends Less_Tree {
public $name;
public $index;
public $currentFileInfo;
public $evaluating = false;
/**
* @param string $name
*/
public function __construct( $name, $index = null, $currentFileInfo = null ) {
$this->name = $name;
$this->index = $index;
$this->currentFileInfo = $currentFileInfo;
}
/**
* @param Less_Environment $env
* @return Less_Tree|Less_Tree_Keyword|Less_Tree_Quoted
* @see less-2.5.3.js#Ruleset.prototype.eval
*/
public function compile( $env ) {
if ( $this->name[1] === '@' ) {
$v = new self( substr( $this->name, 1 ), $this->index + 1, $this->currentFileInfo );
// While some Less_Tree nodes have no 'value', we know these can't occur after a
// variable assignment (would have been a ParseError).
$name = '@' . $v->compile( $env )->value;
} else {
$name = $this->name;
}
if ( $this->evaluating ) {
throw new Less_Exception_Compiler( "Recursive variable definition for " . $name, null, $this->index, $this->currentFileInfo );
}
$this->evaluating = true;
foreach ( $env->frames as $frame ) {
if ( $v = $frame->variable( $name ) ) {
$r = $v->value->compile( $env );
$this->evaluating = false;
return $r;
}
}
throw new Less_Exception_Compiler( "variable " . $name . " is undefined in file " . $this->currentFileInfo["filename"], null, $this->index, $this->currentFileInfo );
}
}

View File

@ -0,0 +1,16 @@
<?php
/**
* Version numbers
*/
class Less_Version {
/* Current release version of less.php */
public const version = '4.1.0';
/* Upstream less.js version that this release should be compatible with */
public const less_version = '2.5.3';
/* Parser cache version */
public const cache_version = '253';
}

View File

@ -0,0 +1,43 @@
<?php
/**
* @private
*/
class Less_Visitor {
protected $methods = [];
protected $_visitFnCache = [];
public function __construct() {
$this->_visitFnCache = get_class_methods( get_class( $this ) );
$this->_visitFnCache = array_flip( $this->_visitFnCache );
}
public function visitObj( $node ) {
$funcName = 'visit' . str_replace( [ 'Less_Tree_', '_' ], '', get_class( $node ) );
if ( isset( $this->_visitFnCache[$funcName] ) ) {
$visitDeeper = true;
$this->$funcName( $node, $visitDeeper );
if ( $visitDeeper ) {
$node->accept( $this );
}
$funcName .= "Out";
if ( isset( $this->_visitFnCache[$funcName] ) ) {
$this->$funcName( $node );
}
} else {
$node->accept( $this );
}
return $node;
}
public function visitArray( $nodes ) {
foreach ( $nodes as $node ) {
$this->visitObj( $node );
}
return $nodes;
}
}

View File

@ -0,0 +1,105 @@
<?php
/**
* @private
*/
class Less_Visitor_extendFinder extends Less_Visitor {
public $contexts = [];
public $allExtendsStack;
public $foundExtends;
public function __construct() {
$this->contexts = [];
$this->allExtendsStack = [ [] ];
parent::__construct();
}
/**
* @param Less_Tree_Ruleset $root
*/
public function run( $root ) {
$root = $this->visitObj( $root );
$root->allExtends =& $this->allExtendsStack[0];
return $root;
}
public function visitRule( $ruleNode, &$visitDeeper ) {
$visitDeeper = false;
}
public function visitMixinDefinition( $mixinDefinitionNode, &$visitDeeper ) {
$visitDeeper = false;
}
public function visitRuleset( $rulesetNode ) {
if ( $rulesetNode->root ) {
return;
}
$allSelectorsExtendList = [];
// get &:extend(.a); rules which apply to all selectors in this ruleset
if ( $rulesetNode->rules ) {
foreach ( $rulesetNode->rules as $rule ) {
if ( $rule instanceof Less_Tree_Extend ) {
$allSelectorsExtendList[] = $rule;
$rulesetNode->extendOnEveryPath = true;
}
}
}
// now find every selector and apply the extends that apply to all extends
// and the ones which apply to an individual extend
foreach ( $rulesetNode->paths as $selectorPath ) {
$selector = end( $selectorPath ); // $selectorPath[ count($selectorPath)-1];
$j = 0;
foreach ( $selector->extendList as $extend ) {
$this->allExtendsStackPush( $rulesetNode, $selectorPath, $extend, $j );
}
foreach ( $allSelectorsExtendList as $extend ) {
$this->allExtendsStackPush( $rulesetNode, $selectorPath, $extend, $j );
}
}
$this->contexts[] = $rulesetNode->selectors;
}
public function allExtendsStackPush( $rulesetNode, $selectorPath, Less_Tree_Extend $extend, &$j ) {
$this->foundExtends = true;
$extend = $extend->clone();
$extend->findSelfSelectors( $selectorPath );
$extend->ruleset = $rulesetNode;
if ( $j === 0 ) {
$extend->firstExtendOnThisSelectorPath = true;
}
$end_key = count( $this->allExtendsStack ) - 1;
$this->allExtendsStack[$end_key][] = $extend;
$j++;
}
public function visitRulesetOut( $rulesetNode ) {
if ( !is_object( $rulesetNode ) || !$rulesetNode->root ) {
array_pop( $this->contexts );
}
}
public function visitMedia( $mediaNode ) {
$mediaNode->allExtends = [];
$this->allExtendsStack[] =& $mediaNode->allExtends;
}
public function visitMediaOut() {
array_pop( $this->allExtendsStack );
}
public function visitDirective( $directiveNode ) {
$directiveNode->allExtends = [];
$this->allExtendsStack[] =& $directiveNode->allExtends;
}
public function visitDirectiveOut() {
array_pop( $this->allExtendsStack );
}
}

View File

@ -0,0 +1,137 @@
<?php
/*
class Less_Visitor_import extends Less_VisitorReplacing{
public $_visitor;
public $_importer;
public $importCount;
function __construct( $evalEnv ){
$this->env = $evalEnv;
$this->importCount = 0;
parent::__construct();
}
function run( $root ){
$root = $this->visitObj($root);
$this->isFinished = true;
//if( $this->importCount === 0) {
// $this->_finish();
//}
}
function visitImport($importNode, &$visitDeeper ){
$importVisitor = $this;
$inlineCSS = $importNode->options['inline'];
if( !$importNode->css || $inlineCSS ){
$evaldImportNode = $importNode->compileForImport($this->env);
if( $evaldImportNode && (!$evaldImportNode->css || $inlineCSS) ){
$importNode = $evaldImportNode;
$this->importCount++;
$env = clone $this->env;
if( (isset($importNode->options['multiple']) && $importNode->options['multiple']) ){
$env->importMultiple = true;
}
//get path & uri
$path_and_uri = null;
if( is_callable(Less_Parser::$options['import_callback']) ){
$path_and_uri = call_user_func(Less_Parser::$options['import_callback'],$importNode);
}
if( !$path_and_uri ){
$path_and_uri = $importNode->PathAndUri();
}
if( $path_and_uri ){
list($full_path, $uri) = $path_and_uri;
}else{
$full_path = $uri = $importNode->getPath();
}
//import once
if( $importNode->skip( $full_path, $env) ){
return array();
}
if( $importNode->options['inline'] ){
//todo needs to reference css file not import
//$contents = new Less_Tree_Anonymous($importNode->root, 0, array('filename'=>$importNode->importedFilename), true );
Less_Parser::AddParsedFile($full_path);
$contents = new Less_Tree_Anonymous( file_get_contents($full_path), 0, array(), true );
if( $importNode->features ){
return new Less_Tree_Media( array($contents), $importNode->features->value );
}
return array( $contents );
}
// css ?
if( $importNode->css ){
$features = ( $importNode->features ? $importNode->features->compile($env) : null );
return new Less_Tree_Import( $importNode->compilePath( $env), $features, $importNode->options, $this->index);
}
return $importNode->ParseImport( $full_path, $uri, $env );
}
}
$visitDeeper = false;
return $importNode;
}
function visitRule( $ruleNode, &$visitDeeper ){
$visitDeeper = false;
return $ruleNode;
}
function visitDirective($directiveNode, $visitArgs){
array_unshift($this->env->frames,$directiveNode);
return $directiveNode;
}
function visitDirectiveOut($directiveNode) {
array_shift($this->env->frames);
}
function visitMixinDefinition($mixinDefinitionNode, $visitArgs) {
array_unshift($this->env->frames,$mixinDefinitionNode);
return $mixinDefinitionNode;
}
function visitMixinDefinitionOut($mixinDefinitionNode) {
array_shift($this->env->frames);
}
function visitRuleset($rulesetNode, $visitArgs) {
array_unshift($this->env->frames,$rulesetNode);
return $rulesetNode;
}
function visitRulesetOut($rulesetNode) {
array_shift($this->env->frames);
}
function visitMedia($mediaNode, $visitArgs) {
array_unshift($this->env->frames, $mediaNode->ruleset);
return $mediaNode;
}
function visitMediaOut($mediaNode) {
array_shift($this->env->frames);
}
}
*/

View File

@ -0,0 +1,75 @@
<?php
/**
* @private
*/
class Less_Visitor_joinSelector extends Less_Visitor {
public $contexts = [ [] ];
/**
* @param Less_Tree_Ruleset $root
*/
public function run( $root ) {
return $this->visitObj( $root );
}
public function visitRule( $ruleNode, &$visitDeeper ) {
$visitDeeper = false;
}
public function visitMixinDefinition( $mixinDefinitionNode, &$visitDeeper ) {
$visitDeeper = false;
}
public function visitRuleset( $rulesetNode ) {
$context = end( $this->contexts );
$paths = [];
if ( !$rulesetNode->root ) {
$selectors = $rulesetNode->selectors;
if ( $selectors !== null ) {
$filtered = [];
foreach ( $selectors as $selector ) {
if ( $selector->getIsOutput() ) {
$filtered[] = $selector;
}
}
$selectors = $rulesetNode->selectors = $filtered ?: null;
if ( $selectors ) {
$paths = $rulesetNode->joinSelectors( $context, $selectors );
}
}
if ( $selectors === null ) {
$rulesetNode->rules = null;
}
$rulesetNode->paths = $paths;
}
// NOTE: Assigned here instead of at the start like less.js,
// because PHP arrays aren't by-ref
$this->contexts[] = $paths;
}
public function visitRulesetOut() {
array_pop( $this->contexts );
}
public function visitMedia( $mediaNode ) {
$context = end( $this->contexts );
if ( count( $context ) === 0 || ( is_object( $context[0] ) && $context[0]->multiMedia ) ) {
$mediaNode->rules[0]->root = true;
}
}
public function visitDirective( $directiveNode ) {
$context = end( $this->contexts );
if ( $directiveNode->rules && count( $directiveNode->rules ) > 0 ) {
$directiveNode->rules[0]->root = $directiveNode->isRooted || count( $context ) === 0;
}
}
}

View File

@ -0,0 +1,464 @@
<?php
/**
* @private
*/
class Less_Visitor_processExtends extends Less_Visitor {
public $allExtendsStack;
/**
* @param Less_Tree_Ruleset $root
*/
public function run( $root ) {
$extendFinder = new Less_Visitor_extendFinder();
$extendFinder->run( $root );
if ( !$extendFinder->foundExtends ) {
return $root;
}
$root->allExtends = $this->doExtendChaining( $root->allExtends, $root->allExtends );
$this->allExtendsStack = [];
$this->allExtendsStack[] = &$root->allExtends;
return $this->visitObj( $root );
}
private function doExtendChaining( $extendsList, $extendsListTarget, $iterationCount = 0 ) {
//
// chaining is different from normal extension.. if we extend an extend then we are not just copying, altering and pasting
// the selector we would do normally, but we are also adding an extend with the same target selector
// this means this new extend can then go and alter other extends
//
// this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors
// this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already processed if
// we look at each selector at a time, as is done in visitRuleset
$extendsToAdd = [];
// loop through comparing every extend with every target extend.
// a target extend is the one on the ruleset we are looking at copy/edit/pasting in place
// e.g. .a:extend(.b) {} and .b:extend(.c) {} then the first extend extends the second one
// and the second is the target.
// the separation into two lists allows us to process a subset of chains with a bigger set, as is the
// case when processing media queries
for ( $extendIndex = 0, $extendsList_len = count( $extendsList ); $extendIndex < $extendsList_len; $extendIndex++ ) {
for ( $targetExtendIndex = 0; $targetExtendIndex < count( $extendsListTarget ); $targetExtendIndex++ ) {
$extend = $extendsList[$extendIndex];
$targetExtend = $extendsListTarget[$targetExtendIndex];
// Optimisation: Explicit reference, <https://github.com/wikimedia/less.php/pull/14>
if ( \array_key_exists( $targetExtend->object_id, $extend->parent_ids ) ) {
// ignore circular references
continue;
}
// find a match in the target extends self selector (the bit before :extend)
$selectorPath = [ $targetExtend->selfSelectors[0] ];
$matches = $this->findMatch( $extend, $selectorPath );
if ( $matches ) {
// we found a match, so for each self selector..
foreach ( $extend->selfSelectors as $selfSelector ) {
// process the extend as usual
$newSelector = $this->extendSelector( $matches, $selectorPath, $selfSelector );
// but now we create a new extend from it
$newExtend = new Less_Tree_Extend( $targetExtend->selector, $targetExtend->option, 0 );
$newExtend->selfSelectors = $newSelector;
// add the extend onto the list of extends for that selector
end( $newSelector )->extendList = [ $newExtend ];
// $newSelector[ count($newSelector)-1]->extendList = array($newExtend);
// record that we need to add it.
$extendsToAdd[] = $newExtend;
$newExtend->ruleset = $targetExtend->ruleset;
// remember its parents for circular references
$newExtend->parent_ids = array_merge( $newExtend->parent_ids, $targetExtend->parent_ids, $extend->parent_ids );
// only process the selector once.. if we have :extend(.a,.b) then multiple
// extends will look at the same selector path, so when extending
// we know that any others will be duplicates in terms of what is added to the css
if ( $targetExtend->firstExtendOnThisSelectorPath ) {
$newExtend->firstExtendOnThisSelectorPath = true;
$targetExtend->ruleset->paths[] = $newSelector;
}
}
}
}
}
if ( $extendsToAdd ) {
// try to detect circular references to stop a stack overflow.
// may no longer be needed. $this->extendChainCount++;
if ( $iterationCount > 100 ) {
try{
$selectorOne = $extendsToAdd[0]->selfSelectors[0]->toCSS();
$selectorTwo = $extendsToAdd[0]->selector->toCSS();
}catch ( Exception $e ) {
$selectorOne = "{unable to calculate}";
$selectorTwo = "{unable to calculate}";
}
throw new Less_Exception_Parser( "extend circular reference detected. One of the circular extends is currently:" . $selectorOne . ":extend(" . $selectorTwo . ")" );
}
// now process the new extends on the existing rules so that we can handle a extending b extending c ectending d extending e...
$extendsToAdd = $this->doExtendChaining( $extendsToAdd, $extendsListTarget, $iterationCount + 1 );
}
return array_merge( $extendsList, $extendsToAdd );
}
protected function visitRule( $ruleNode, &$visitDeeper ) {
$visitDeeper = false;
}
protected function visitMixinDefinition( $mixinDefinitionNode, &$visitDeeper ) {
$visitDeeper = false;
}
protected function visitSelector( $selectorNode, &$visitDeeper ) {
$visitDeeper = false;
}
protected function visitRuleset( $rulesetNode ) {
if ( $rulesetNode->root ) {
return;
}
$allExtends = end( $this->allExtendsStack );
$paths_len = count( $rulesetNode->paths );
// look at each selector path in the ruleset, find any extend matches and then copy, find and replace
foreach ( $allExtends as $allExtend ) {
for ( $pathIndex = 0; $pathIndex < $paths_len; $pathIndex++ ) {
// extending extends happens initially, before the main pass
if ( isset( $rulesetNode->extendOnEveryPath ) && $rulesetNode->extendOnEveryPath ) {
continue;
}
$selectorPath = $rulesetNode->paths[$pathIndex];
if ( end( $selectorPath )->extendList ) {
continue;
}
$this->ExtendMatch( $rulesetNode, $allExtend, $selectorPath );
}
}
}
private function ExtendMatch( $rulesetNode, $extend, $selectorPath ) {
$matches = $this->findMatch( $extend, $selectorPath );
if ( $matches ) {
foreach ( $extend->selfSelectors as $selfSelector ) {
$rulesetNode->paths[] = $this->extendSelector( $matches, $selectorPath, $selfSelector );
}
}
}
/**
* @param Less_Tree_Extend $extend
* @param Less_Tree_Selector[] $haystackSelectorPath
* @return false|array<array{index:int,initialCombinator:string}>
*/
private function findMatch( $extend, $haystackSelectorPath ) {
if ( !$this->HasMatches( $extend, $haystackSelectorPath ) ) {
return false;
}
//
// look through the haystack selector path to try and find the needle - extend.selector
// returns an array of selector matches that can then be replaced
//
$needleElements = $extend->selector->elements;
$potentialMatches = [];
$potentialMatches_len = 0;
$potentialMatch = null;
$matches = [];
// loop through the haystack elements
$haystack_path_len = count( $haystackSelectorPath );
for ( $haystackSelectorIndex = 0; $haystackSelectorIndex < $haystack_path_len; $haystackSelectorIndex++ ) {
$hackstackSelector = $haystackSelectorPath[$haystackSelectorIndex];
$haystack_elements_len = count( $hackstackSelector->elements );
for ( $hackstackElementIndex = 0; $hackstackElementIndex < $haystack_elements_len; $hackstackElementIndex++ ) {
$haystackElement = $hackstackSelector->elements[$hackstackElementIndex];
// if we allow elements before our match we can add a potential match every time. otherwise only at the first element.
if ( $extend->allowBefore || ( $haystackSelectorIndex === 0 && $hackstackElementIndex === 0 ) ) {
$potentialMatches[] = [ 'pathIndex' => $haystackSelectorIndex, 'index' => $hackstackElementIndex, 'matched' => 0, 'initialCombinator' => $haystackElement->combinator ];
$potentialMatches_len++;
}
for ( $i = 0; $i < $potentialMatches_len; $i++ ) {
$potentialMatch = &$potentialMatches[$i];
$potentialMatch = $this->PotentialMatch( $potentialMatch, $needleElements, $haystackElement, $hackstackElementIndex );
// if we are still valid and have finished, test whether we have elements after and whether these are allowed
if ( $potentialMatch && $potentialMatch['matched'] === $extend->selector->elements_len ) {
$potentialMatch['finished'] = true;
if ( !$extend->allowAfter && ( $hackstackElementIndex + 1 < $haystack_elements_len || $haystackSelectorIndex + 1 < $haystack_path_len ) ) {
$potentialMatch = null;
}
}
// if null we remove, if not, we are still valid, so either push as a valid match or continue
if ( $potentialMatch ) {
if ( $potentialMatch['finished'] ) {
$potentialMatch['length'] = $extend->selector->elements_len;
$potentialMatch['endPathIndex'] = $haystackSelectorIndex;
$potentialMatch['endPathElementIndex'] = $hackstackElementIndex + 1; // index after end of match
$potentialMatches = []; // we don't allow matches to overlap, so start matching again
$potentialMatches_len = 0;
$matches[] = $potentialMatch;
}
continue;
}
array_splice( $potentialMatches, $i, 1 );
$potentialMatches_len--;
$i--;
}
}
}
return $matches;
}
// Before going through all the nested loops, lets check to see if a match is possible
// Reduces Bootstrap 3.1 compile time from ~6.5s to ~5.6s
private function HasMatches( $extend, $haystackSelectorPath ) {
if ( !$extend->selector->cacheable ) {
return true;
}
$first_el = $extend->selector->_oelements[0];
foreach ( $haystackSelectorPath as $hackstackSelector ) {
if ( !$hackstackSelector->cacheable ) {
return true;
}
// Optimisation: Explicit reference, <https://github.com/wikimedia/less.php/pull/14>
if ( \array_key_exists( $first_el, $hackstackSelector->_oelements_assoc ) ) {
return true;
}
}
return false;
}
/**
* @param array $potentialMatch
* @param Less_Tree_Element[] $needleElements
* @param Less_Tree_Element $haystackElement
* @param int $hackstackElementIndex
*/
private function PotentialMatch( $potentialMatch, $needleElements, $haystackElement, $hackstackElementIndex ) {
if ( $potentialMatch['matched'] > 0 ) {
// selectors add " " onto the first element. When we use & it joins the selectors together, but if we don't
// then each selector in haystackSelectorPath has a space before it added in the toCSS phase. so we need to work out
// what the resulting combinator will be
$targetCombinator = $haystackElement->combinator;
if ( $targetCombinator === '' && $hackstackElementIndex === 0 ) {
$targetCombinator = ' ';
}
if ( $needleElements[ $potentialMatch['matched'] ]->combinator !== $targetCombinator ) {
return null;
}
}
// if we don't match, null our match to indicate failure
if ( !$this->isElementValuesEqual( $needleElements[$potentialMatch['matched'] ]->value, $haystackElement->value ) ) {
return null;
}
$potentialMatch['finished'] = false;
$potentialMatch['matched']++;
return $potentialMatch;
}
/**
* @param string|Less_Tree_Attribute|Less_Tree_Dimension|Less_Tree_Keyword $elementValue1
* @param string|Less_Tree_Attribute|Less_Tree_Dimension|Less_Tree_Keyword $elementValue2
* @return bool
*/
private function isElementValuesEqual( $elementValue1, $elementValue2 ) {
if ( $elementValue1 === $elementValue2 ) {
return true;
}
if ( is_string( $elementValue1 ) || is_string( $elementValue2 ) ) {
return false;
}
if ( $elementValue1 instanceof Less_Tree_Attribute ) {
return $this->isAttributeValuesEqual( $elementValue1, $elementValue2 );
}
$elementValue1 = $elementValue1->value;
if ( $elementValue1 instanceof Less_Tree_Selector ) {
return $this->isSelectorValuesEqual( $elementValue1, $elementValue2 );
}
return false;
}
/**
* @param Less_Tree_Selector $elementValue1
*/
private function isSelectorValuesEqual( $elementValue1, $elementValue2 ) {
$elementValue2 = $elementValue2->value;
if ( !( $elementValue2 instanceof Less_Tree_Selector ) || $elementValue1->elements_len !== $elementValue2->elements_len ) {
return false;
}
for ( $i = 0; $i < $elementValue1->elements_len; $i++ ) {
if ( $elementValue1->elements[$i]->combinator !== $elementValue2->elements[$i]->combinator ) {
if ( $i !== 0 || ( $elementValue1->elements[$i]->combinator || ' ' ) !== ( $elementValue2->elements[$i]->combinator || ' ' ) ) {
return false;
}
}
if ( !$this->isElementValuesEqual( $elementValue1->elements[$i]->value, $elementValue2->elements[$i]->value ) ) {
return false;
}
}
return true;
}
/**
* @param Less_Tree_Attribute $elementValue1
*/
private function isAttributeValuesEqual( $elementValue1, $elementValue2 ) {
if ( $elementValue1->op !== $elementValue2->op || $elementValue1->key !== $elementValue2->key ) {
return false;
}
if ( !$elementValue1->value || !$elementValue2->value ) {
if ( $elementValue1->value || $elementValue2->value ) {
return false;
}
return true;
}
$elementValue1 = $elementValue1->value;
if ( $elementValue1 instanceof Less_Tree_Quoted ) {
$elementValue1 = $elementValue1->value;
}
$elementValue2 = $elementValue2->value;
if ( $elementValue2 instanceof Less_Tree_Quoted ) {
$elementValue2 = $elementValue2->value;
}
return $elementValue1 === $elementValue2;
}
private function extendSelector( $matches, $selectorPath, $replacementSelector ) {
// for a set of matches, replace each match with the replacement selector
$currentSelectorPathIndex = 0;
$currentSelectorPathElementIndex = 0;
$path = [];
$selectorPath_len = count( $selectorPath );
for ( $matchIndex = 0, $matches_len = count( $matches ); $matchIndex < $matches_len; $matchIndex++ ) {
$match = $matches[$matchIndex];
$selector = $selectorPath[ $match['pathIndex'] ];
$firstElement = new Less_Tree_Element(
$match['initialCombinator'],
$replacementSelector->elements[0]->value,
$replacementSelector->elements[0]->index,
$replacementSelector->elements[0]->currentFileInfo
);
if ( $match['pathIndex'] > $currentSelectorPathIndex && $currentSelectorPathElementIndex > 0 ) {
$last_path = end( $path );
$last_path->elements = array_merge( $last_path->elements, array_slice( $selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex ) );
$currentSelectorPathElementIndex = 0;
$currentSelectorPathIndex++;
}
$newElements = array_merge(
array_slice(
$selector->elements,
$currentSelectorPathElementIndex,
// last parameter of array_slice is different than the last parameter of javascript's slice
$match['index'] - $currentSelectorPathElementIndex
),
[ $firstElement ],
array_slice( $replacementSelector->elements, 1 )
);
if ( $currentSelectorPathIndex === $match['pathIndex'] && $matchIndex > 0 ) {
$last_key = count( $path ) - 1;
$path[$last_key]->elements = array_merge( $path[$last_key]->elements, $newElements );
} else {
$path = array_merge( $path, array_slice( $selectorPath, $currentSelectorPathIndex, $match['pathIndex'] ) );
$path[] = new Less_Tree_Selector( $newElements );
}
$currentSelectorPathIndex = $match['endPathIndex'];
$currentSelectorPathElementIndex = $match['endPathElementIndex'];
if ( $currentSelectorPathElementIndex >= count( $selectorPath[$currentSelectorPathIndex]->elements ) ) {
$currentSelectorPathElementIndex = 0;
$currentSelectorPathIndex++;
}
}
if ( $currentSelectorPathIndex < $selectorPath_len && $currentSelectorPathElementIndex > 0 ) {
$last_path = end( $path );
$last_path->elements = array_merge( $last_path->elements, array_slice( $selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex ) );
$currentSelectorPathIndex++;
}
$slice_len = $selectorPath_len - $currentSelectorPathIndex;
$path = array_merge( $path, array_slice( $selectorPath, $currentSelectorPathIndex, $slice_len ) );
return $path;
}
protected function visitMedia( $mediaNode ) {
$newAllExtends = array_merge( $mediaNode->allExtends, end( $this->allExtendsStack ) );
$this->allExtendsStack[] = $this->doExtendChaining( $newAllExtends, $mediaNode->allExtends );
}
protected function visitMediaOut() {
array_pop( $this->allExtendsStack );
}
protected function visitDirective( $directiveNode ) {
$newAllExtends = array_merge( $directiveNode->allExtends, end( $this->allExtendsStack ) );
$this->allExtendsStack[] = $this->doExtendChaining( $newAllExtends, $directiveNode->allExtends );
}
protected function visitDirectiveOut() {
array_pop( $this->allExtendsStack );
}
}

View File

@ -0,0 +1,276 @@
<?php
/**
* @private
*/
class Less_Visitor_toCSS extends Less_VisitorReplacing {
private $charset;
public function __construct() {
parent::__construct();
}
/**
* @param Less_Tree_Ruleset $root
*/
public function run( $root ) {
return $this->visitObj( $root );
}
public function visitRule( $ruleNode ) {
if ( $ruleNode->variable ) {
return [];
}
return $ruleNode;
}
public function visitMixinDefinition( $mixinNode ) {
// mixin definitions do not get eval'd - this means they keep state
// so we have to clear that state here so it isn't used if toCSS is called twice
$mixinNode->frames = [];
return [];
}
public function visitExtend() {
return [];
}
public function visitComment( $commentNode ) {
if ( $commentNode->isSilent() ) {
return [];
}
return $commentNode;
}
public function visitMedia( $mediaNode, &$visitDeeper ) {
$mediaNode->accept( $this );
$visitDeeper = false;
if ( !$mediaNode->rules ) {
return [];
}
return $mediaNode;
}
public function visitDirective( $directiveNode ) {
if ( isset( $directiveNode->currentFileInfo['reference'] ) && ( !property_exists( $directiveNode, 'isReferenced' ) || !$directiveNode->isReferenced ) ) {
return [];
}
if ( $directiveNode->name === '@charset' ) {
// Only output the debug info together with subsequent @charset definitions
// a comment (or @media statement) before the actual @charset directive would
// be considered illegal css as it has to be on the first line
if ( isset( $this->charset ) && $this->charset ) {
// if( $directiveNode->debugInfo ){
// $comment = new Less_Tree_Comment('/* ' . str_replace("\n",'',$directiveNode->toCSS())." */\n");
// $comment->debugInfo = $directiveNode->debugInfo;
// return $this->visit($comment);
//}
return [];
}
$this->charset = true;
}
return $directiveNode;
}
public function checkPropertiesInRoot( $rulesetNode ) {
if ( !$rulesetNode->firstRoot ) {
return;
}
foreach ( $rulesetNode->rules as $ruleNode ) {
if ( $ruleNode instanceof Less_Tree_Rule && !$ruleNode->variable ) {
$msg = "properties must be inside selector blocks, they cannot be in the root. Index " . $ruleNode->index .
( $ruleNode->currentFileInfo ? ' Filename: ' . $ruleNode->currentFileInfo['filename'] : null );
throw new Less_Exception_Compiler( $msg );
}
}
}
public function visitRuleset( $rulesetNode, &$visitDeeper ) {
$visitDeeper = false;
$this->checkPropertiesInRoot( $rulesetNode );
if ( $rulesetNode->root ) {
return $this->visitRulesetRoot( $rulesetNode );
}
$rulesets = [];
$rulesetNode->paths = $this->visitRulesetPaths( $rulesetNode );
// Compile rules and rulesets
$nodeRuleCnt = $rulesetNode->rules ? count( $rulesetNode->rules ) : 0;
for ( $i = 0; $i < $nodeRuleCnt; ) {
$rule = $rulesetNode->rules[$i];
if ( property_exists( $rule, 'rules' ) ) {
// visit because we are moving them out from being a child
$rulesets[] = $this->visitObj( $rule );
array_splice( $rulesetNode->rules, $i, 1 );
$nodeRuleCnt--;
continue;
}
$i++;
}
// accept the visitor to remove rules and refactor itself
// then we can decide now whether we want it or not
if ( $nodeRuleCnt > 0 ) {
$rulesetNode->accept( $this );
if ( $rulesetNode->rules ) {
if ( count( $rulesetNode->rules ) > 1 ) {
$this->_mergeRules( $rulesetNode->rules );
$this->_removeDuplicateRules( $rulesetNode->rules );
}
// now decide whether we keep the ruleset
if ( $rulesetNode->paths ) {
// array_unshift($rulesets, $rulesetNode);
array_splice( $rulesets, 0, 0, [ $rulesetNode ] );
}
}
}
if ( count( $rulesets ) === 1 ) {
return $rulesets[0];
}
return $rulesets;
}
/**
* Helper function for visitiRuleset
*
* return array|Less_Tree_Ruleset
*/
private function visitRulesetRoot( $rulesetNode ) {
$rulesetNode->accept( $this );
if ( $rulesetNode->firstRoot || $rulesetNode->rules ) {
return $rulesetNode;
}
return [];
}
/**
* Helper function for visitRuleset()
*
* @return array
*/
private function visitRulesetPaths( $rulesetNode ) {
$paths = [];
foreach ( $rulesetNode->paths as $p ) {
if ( $p[0]->elements[0]->combinator === ' ' ) {
$p[0]->elements[0]->combinator = '';
}
foreach ( $p as $pi ) {
if ( $pi->getIsReferenced() && $pi->getIsOutput() ) {
$paths[] = $p;
break;
}
}
}
return $paths;
}
protected function _removeDuplicateRules( &$rules ) {
// remove duplicates
$ruleCache = [];
for ( $i = count( $rules ) - 1; $i >= 0; $i-- ) {
$rule = $rules[$i];
if ( $rule instanceof Less_Tree_Rule || $rule instanceof Less_Tree_NameValue ) {
if ( !isset( $ruleCache[$rule->name] ) ) {
$ruleCache[$rule->name] = $rule;
} else {
$ruleList =& $ruleCache[$rule->name];
if ( $ruleList instanceof Less_Tree_Rule || $ruleList instanceof Less_Tree_NameValue ) {
$ruleList = $ruleCache[$rule->name] = [ $ruleCache[$rule->name]->toCSS() ];
}
$ruleCSS = $rule->toCSS();
if ( in_array( $ruleCSS, $ruleList ) ) {
array_splice( $rules, $i, 1 );
} else {
$ruleList[] = $ruleCSS;
}
}
}
}
}
protected function _mergeRules( &$rules ) {
$groups = [];
// obj($rules);
$rules_len = count( $rules );
for ( $i = 0; $i < $rules_len; $i++ ) {
$rule = $rules[$i];
if ( ( $rule instanceof Less_Tree_Rule ) && $rule->merge ) {
$key = $rule->name;
if ( $rule->important ) {
$key .= ',!';
}
if ( !isset( $groups[$key] ) ) {
$groups[$key] = [];
} else {
array_splice( $rules, $i--, 1 );
$rules_len--;
}
$groups[$key][] = $rule;
}
}
foreach ( $groups as $parts ) {
if ( count( $parts ) > 1 ) {
$rule = $parts[0];
$spacedGroups = [];
$lastSpacedGroup = [];
$parts_mapped = [];
foreach ( $parts as $p ) {
if ( $p->merge === '+' ) {
if ( $lastSpacedGroup ) {
$spacedGroups[] = self::toExpression( $lastSpacedGroup );
}
$lastSpacedGroup = [];
}
$lastSpacedGroup[] = $p;
}
$spacedGroups[] = self::toExpression( $lastSpacedGroup );
$rule->value = self::toValue( $spacedGroups );
}
}
}
public static function toExpression( $values ) {
$mapped = [];
foreach ( $values as $p ) {
$mapped[] = $p->value;
}
return new Less_Tree_Expression( $mapped );
}
public static function toValue( $values ) {
// return new Less_Tree_Value($values); ??
$mapped = [];
foreach ( $values as $p ) {
$mapped[] = $p;
}
return new Less_Tree_Value( $mapped );
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* @private
*/
class Less_VisitorReplacing extends Less_Visitor {
public function visitObj( $node ) {
$funcName = 'visit' . str_replace( [ 'Less_Tree_', '_' ], '', get_class( $node ) );
if ( isset( $this->_visitFnCache[$funcName] ) ) {
$visitDeeper = true;
$node = $this->$funcName( $node, $visitDeeper );
if ( $node ) {
if ( $visitDeeper && is_object( $node ) ) {
$node->accept( $this );
}
$funcName .= "Out";
if ( isset( $this->_visitFnCache[$funcName] ) ) {
$this->$funcName( $node );
}
}
} else {
$node->accept( $this );
}
return $node;
}
public function visitArray( $nodes ) {
$newNodes = [];
foreach ( $nodes as $node ) {
$evald = $this->visitObj( $node );
if ( $evald ) {
if ( is_array( $evald ) ) {
self::flatten( $evald, $newNodes );
} else {
$newNodes[] = $evald;
}
}
}
return $newNodes;
}
public function flatten( $arr, &$out ) {
foreach ( $arr as $item ) {
if ( !is_array( $item ) ) {
$out[] = $item;
continue;
}
foreach ( $item as $nestedItem ) {
if ( is_array( $nestedItem ) ) {
self::flatten( $nestedItem, $out );
} else {
$out[] = $nestedItem;
}
}
}
return $out;
}
}

View File

@ -0,0 +1,6 @@
<?php
require_once ("Less/Autoloader.php");
Less_Autoloader::register();
?>

View File

@ -810,6 +810,9 @@
}
}
*/
if ($page->FileName === null)
continue;
$vars = System::ParsePathVariables($page->FileName, System::GetVirtualPathString());
if ($vars !== false)
{
@ -820,6 +823,8 @@
}
}
if (count($actualPages) > 0)
{
// FIXME : find the "best match" for the given route
$qs = new QuickSort();
$sortedPages = $qs->Sort($actualPages, function($left, $right)
@ -830,9 +835,14 @@
return $val;
});
$actualPage = $sortedPages[0];
if ($actualPage->FileName !== null)
{
$pathVars = System::ParsePathVariables($actualPage->FileName, System::GetVirtualPathString());
}
return true;
}
return false;
}
/**
* Parses a path in the form /static/routes/{with}/dynamic/{variables}.htmld
@ -1188,7 +1198,8 @@
require_once("Utilities/Stopwatch.inc.php");
require_once("Compilers/StyleSheet/Internal/LessStyleSheetCompiler.inc.php");
// require_once("Compilers/StyleSheet/LESS/Internal/LessStyleSheetCompiler.inc.php");
require_once("Compilers/StyleSheet/LESS/Wikimedia/WikimediaLessStyleSheetCompiler.inc.php");
require("WebApplication.inc.php");
require("WebApplicationTask.inc.php");

View File

@ -1,41 +1,236 @@
<?php
namespace Phast;
class UUID
class UUIDParseFailureKind
{
protected $urand;
const FORMAT = 2;
}
class UUIDParseNumbers
{
const NOSPACE = 2;
}
class UUIDParseResult
{
public $Kind;
public $Message;
public UUID $parsedGuid;
public function __construct()
{
$this->urand = @fopen ( '/dev/urandom', 'rb' );
$this->parsedGuid = new UUID();
}
public static function Generate()
public function setFailure(/*UUIDParseFailureKind*/ $kind, $message)
{
$uuid = new UUID();
return $uuid->get();
$this->Kind = $kind;
$this->Message = $message;
}
}
/**
* @brief Generates a Universally Unique IDentifier, version 4.
*
* This function generates a truly random UUID. The built in CakePHP String::uuid() function
* is not cryptographically secure. You should uses this function instead.
*
* @see http://tools.ietf.org/html/rfc4122#section-4.4
* @see http://en.wikipedia.org/wiki/UUID
* @return string A UUID, made up of 32 hex digits and 4 hyphens.
class UUID
{
private static $urand = null;
public function __construct()
{
if (UUID::$urand == null)
{
UUID::$urand = @fopen ( '/dev/urandom', 'rb' );
}
$this->_a = 0;
$this->_b = 0;
$this->_c = 0;
$this->_d = 0;
$this->_e = 0;
$this->_f = 0;
$this->_g = 0;
$this->_h = 0;
$this->_i = 0;
$this->_j = 0;
$this->_k = 0;
}
private static function StringToInt(string $guidString, int &$parsePos, int $requiredLength, int $flags, int &$result, UUIDParseResult &$parseResult)
{
$parseWhat = substr($guidString, $parsePos, $requiredLength);
$result = intval($parseWhat, 16);
$parsePos += $requiredLength;
return true;
}
private static function StringToLong(string $guidString, int &$parsePos, int $flags, int &$result, UUIDParseResult &$parseResult)
{
return UUID::StringToInt($guidString, $parsePos, 16, $flags, $result, $parseResult);
}
private static function TryParseGuidWithDashes(string $guidString) : UUIDParseResult
{
$startPos = 0;
$temp = 0;
$templ = 0;
$currentPos = 0;
$result = new UUIDParseResult();
$hasDashes = true;
// check to see that it's the proper length
if ($guidString[0] == '{')
{
if (strlen($guidString) != 38 || $guidString[37] != '}')
{
$result->setFailure(UUIDParseFailureKind::FORMAT, "Format_GuidInvLen[{38]");
return $result;
}
$startPos = 1;
}
else if ($guidString[0] == '(')
{
if (strlen($guidString) != 38 || $guidString[37] != ')')
{
$result->setFailure(UUIDParseFailureKind::FORMAT, "Format_GuidInvLen[(38]");
return $result;
}
$startPos = 1;
}
else if (strlen($guidString) != 36)
{
if (strlen($guidString) != 32)
{
$result->setFailure(UUIDParseFailureKind::FORMAT, "Format_GuidInvLen[36]");
return $result;
}
else
{
$hasDashes = false;
}
}
if ($hasDashes)
{
if ($guidString[8 + $startPos] != '-' ||
$guidString[13 + $startPos] != '-' ||
$guidString[18 + $startPos] != '-' ||
$guidString[23 + $startPos] != '-') {
$result->setFailure(UUIDParseFailureKind::FORMAT, "Format_GuidDashes");
return $result;
}
}
$currentPos = $startPos;
if (!UUID::StringToInt($guidString, $currentPos, 8, UUIDParseNumbers::NOSPACE, $temp, $result))
{
return $result;
}
$result->parsedGuid->_a = $temp;
if ($hasDashes)
{
++$currentPos; //Increment past the '-';
}
if (!UUID::StringToInt($guidString, $currentPos, 4, UUIDParseNumbers::NOSPACE, $temp, $result))
return $result;
$result->parsedGuid->_b = $temp;
if ($hasDashes)
{
++$currentPos; //Increment past the '-';
}
if (!UUID::StringToInt($guidString, $currentPos, 4, UUIDParseNumbers::NOSPACE, $temp, $result))
return $result;
$result->parsedGuid->_c = $temp;
if ($hasDashes)
{
++$currentPos; //Increment past the '-';
}
if (!UUID::StringToInt($guidString, $currentPos, 4, UUIDParseNumbers::NOSPACE, $temp, $result))
return $result;
if ($hasDashes)
{
++$currentPos; //Increment past the '-';
}
$startPos = $currentPos;
if (!UUID::StringToLong($guidString, $currentPos, UUIDParseNumbers::NOSPACE, $templ, $result)) {
$result->setFailure(UUIDParseFailureKind::FORMAT, "Format_StrToLong");
return $result;
}
/*
if ($currentPos - $startPos != 12) {
$result->setFailure(UUIDParseFailureKind::FORMAT, "Format_GuidInvLen(*)");
return $result;
}
*/
function get()
{
$result->parsedGuid->_d = $temp >> 8;
$result->parsedGuid->_e = $temp;
$temp = ($templ >> 32);
$result->parsedGuid->_f = ($temp >> 8);
$result->parsedGuid->_g = ($temp);
$temp = ($templ);
$result->parsedGuid->_h = ($temp >> 24);
$result->parsedGuid->_i = ($temp >> 16);
$result->parsedGuid->_j = ($temp >> 8);
$result->parsedGuid->_k = ($temp);
return $result;
}
public static function parse(string $value)
{
$result = UUID::TryParseGuidWithDashes($value);
return $result->parsedGuid;
}
public function __is_equal(UUID $uuid)
{
/*
if (
$uuid->clock_seq_hi_and_reserved == $this->clock_seq_hi_and_reserved
&& $uuid->node == $this->node
&& $uuid->time_hi_and_version == $this->time_hi_and_version
&& $uuid->time_low == $this->time_low
&& $uuid->time_mid == $this->time_mid
)
{
return true;
}
*/
if (
$uuid->_a == $this->_a
&& $uuid->_b == $this->_b
&& $uuid->_c == $this->_c
&& $uuid->_d == $this->_d
&& $uuid->_e == $this->_e
&& $uuid->_f == $this->_f
&& $uuid->_g == $this->_g
&& $uuid->_h == $this->_h
&& $uuid->_i == $this->_i
&& $uuid->_j == $this->_j
&& $uuid->_k == $this->_k
)
{
return true;
}
return false;
}
public function __is_not_equal(UUID $uuid)
{
return !$this->__is_equal($uuid);
}
private static function init_bits()
{
$pr_bits = false;
/*
if (is_a ( $this, 'uuid' ))
{
if (is_resource ( $this->urand ))
*/
if (is_resource ( UUID::$urand ))
{
$pr_bits .= @fread ( $this->urand, 16 );
}
$pr_bits .= @fread ( UUID::$urand, 16 );
}
// }
if (! $pr_bits)
{
$fp = @fopen ( '/dev/urandom', 'rb' );
@ -54,6 +249,45 @@
}
}
}
return $pr_bits;
}
/**
* @brief Generates a Universally Unique IDentifier, version 4.
*
* This function generates a truly random UUID. The built in CakePHP String::uuid() function
* is not cryptographically secure. You should uses this function instead.
*
* @see http://tools.ietf.org/html/rfc4122#section-4.4
* @see http://en.wikipedia.org/wiki/UUID
* @return UUID A UUID, made up of 32 hex digits and 4 hyphens.
*/
public static function generate()
{
$uuid = new UUID();
$pr_bits = UUID::init_bits();
$uuid->_a = ((int)$pr_bits[3] << 24) | ((int)$pr_bits[2] << 16) | ((int)$pr_bits[1] << 8) | $pr_bits[0];
$uuid->_b = /*(short)*/(((int)$pr_bits[5] << 8) | $pr_bits[4]);
$uuid->_c = /*(short)*/(((int)$pr_bits[7] << 8) | $pr_bits[6]);
$uuid->_d = $pr_bits[8];
$uuid->_e = $pr_bits[9];
$uuid->_f = $pr_bits[10];
$uuid->_g = $pr_bits[11];
$uuid->_h = $pr_bits[12];
$uuid->_i = $pr_bits[13];
$uuid->_j = $pr_bits[14];
$uuid->_k = $pr_bits[15];
return $uuid;
}
private static function __Generate_Old()
{
$uuid = new UUID();
$pr_bits = UUID::init_bits();
$time_low = bin2hex ( substr ( $pr_bits, 0, 4 ) );
$time_mid = bin2hex ( substr ( $pr_bits, 4, 2 ) );
$time_hi_and_version = bin2hex ( substr ( $pr_bits, 6, 2 ) );
@ -78,10 +312,68 @@
$clock_seq_hi_and_reserved = $clock_seq_hi_and_reserved >> 2;
$clock_seq_hi_and_reserved = $clock_seq_hi_and_reserved | 0x8000;
return strtoupper( sprintf ( '%08s%04s%04x%04x%012s', $time_low, $time_mid, $time_hi_and_version, $clock_seq_hi_and_reserved, $node ) );
$uuid->time_low = $time_low;
$uuid->time_mid = $time_mid;
$uuid->time_hi_and_version = $time_hi_and_version;
$uuid->clock_seq_hi_and_reserved = $clock_seq_hi_and_reserved;
$uuid->node = $node;
return $uuid;
}
public static function format($input)
private $time_low;
private $time_mid;
private $time_hi_and_version;
private $clock_seq_hi_and_reserved;
private $node;
private static function HexToChar(int $a) : string
{
$a = $a & 0xf;
return chr((($a > 9) ? $a - 10 + 0x61 : $a + 0x30));
}
private static function HexsToChars(int $a, int $b, bool $hex = false)
{
$guidChars = "";
if ($hex) {
$guidChars = "0x";
}
$guidChars .= UUID::HexToChar($a>>4);
$guidChars .= UUID::HexToChar($a);
if ($hex)
{
$guidChars .= ",0x";
}
$guidChars .= UUID::HexToChar($b>>4);
$guidChars .= UUID::HexToChar($b);
return $guidChars;
}
private $_a, $_b, $_c, $_d, $_e, $_f, $_g, $_h, $_i, $_j, $_k;
public function __toString()
{
//return $this->format(strtoupper( sprintf ( '%08s%04s%04x%04x%012s', $this->time_low, $this->time_mid, $this->time_hi_and_version, $this->clock_seq_hi_and_reserved, $this->node ) ));
$dash = true;
$guidChars = "{";
$guidChars .= UUID::HexsToChars($this->_a >> 24, $this->_a >> 16);
$guidChars .= UUID::HexsToChars($this->_a >> 8, $this->_a);
if ($dash) $guidChars .= '-';
$guidChars .= UUID::HexsToChars($this->_b >> 8, $this->_b);
if ($dash) $guidChars .= '-';
$guidChars .= UUID::HexsToChars($this->_c >> 8, $this->_c);
if ($dash) $guidChars .= '-';
$guidChars .= UUID::HexsToChars($this->_d, $this->_e);
if ($dash) $guidChars .= '-';
$guidChars .= UUID::HexsToChars($this->_f, $this->_g);
$guidChars .= UUID::HexsToChars($this->_h, $this->_i);
$guidChars .= UUID::HexsToChars($this->_j, $this->_k);
$guidChars .= "}";
return $guidChars;
}
private static function format($input)
{
$output = $input;
$output = substr($output, 0, 8) . "-" . substr($output, 8, 4) . "-" . substr($output, 12, 4) . "-" . substr($output, 16, 4) . "-" . substr($output, 20);

View File

@ -360,6 +360,8 @@
$styleAttributeContent = "";
$classAttributeContent = "";
if (is_array($this->Attributes))
{
$count = count($this->Attributes);
if ($count > 0)
{
@ -412,6 +414,7 @@
$i++;
}
}
}
$styleRules = $this->StyleRules;
if (!$this->Visible)
@ -551,16 +554,21 @@
*/
public function Render()
{
//echo("checking EnableRender");
if ($this->EnableRender !== true) return;
//echo("initializing if not initialized");
if (!$this->Initialized) $this->Initialize();
//echo("checking Enabled");
if ($this->Enabled !== true)
{
$this->Attributes[] = new WebControlAttribute("disabled", "disabled");
}
//echo("creating");
$this->CreateControl();
//echo("begining content");
$this->BeginContent();
if (is_callable($this->Content))
{
@ -572,18 +580,23 @@
}
else
{
//echo("checking controls");
if (count($this->Controls) > 0)
{
//echo("we have");
foreach ($this->Controls as $control)
{
//echo("rendering child");
$control->Render();
}
}
else
{
//echo("rendering content");
$this->RenderContent();
}
}
//echo("ending content");
$this->EndContent();
}

View File

@ -77,26 +77,26 @@
protected function OnInitialize()
{
$this->TagName = "div";
$this->ClassList[] = "AdditionalDetailWidget";
$this->ClassList[] = "uwt-actionpreviewbutton";
if ($this->ShowText)
{
$this->ClassList[] = "Text";
$this->ClassList[] = "apb-show-text";
}
switch ($this->DisplayStyle)
{
case AdditionalDetailWidgetDisplayStyle::Magnify:
{
$this->ClassList[] = "Magnify";
$this->ClassList[] = "apb-style-magnify";
break;
}
case AdditionalDetailWidgetDisplayStyle::Arrow:
{
$this->ClassList[] = "Arrow";
$this->ClassList[] = "apb-style-arrow";
break;
}
case AdditionalDetailWidgetDisplayStyle::Ellipsis:
{
$this->ClassList[] = "Ellipsis";
$this->ClassList[] = "apb-style-ellipsis";
break;
}
}
@ -106,7 +106,7 @@
{
if ($this->ShowURL)
{
echo("<a class=\"AdditionalDetailText\" href=\"");
echo("<a class=\"apb-text\" href=\"");
if ($this->TargetURL != "")
{
echo(System::ExpandRelativePath($this->TargetURL));
@ -128,39 +128,40 @@
}
else
{
echo ("<span class=\"AdditionalDetailText\">" . $this->Text . "</span>");
echo ("<span class=\"apb-text\">" . $this->Text . "</span>");
}
echo("<a class=\"AdditionalDetailButton\">&nbsp;</a>");
echo("<a class=\"apb-button\" href=\"#\" tabindex=\"-1\">&nbsp;</a>");
echo("<div class=\"Content\">");
echo("<div class=\"apb-preview uwt-popup\">");
echo("<div class=\"MenuItems");
echo("<div class=\"apb-actions");
if (count($this->MenuItems) <= 0)
{
echo(" Empty");
echo(" uwt-empty");
}
echo("\">");
echo("<div class=\"Header\">" . $this->MenuItemHeaderText . "</div>");
echo("<h2>" . $this->MenuItemHeaderText . "</h2>");
$divContent = new HTMLControl("div");
$divContent->ClassList[] = "Content";
$menu = new Menu();
foreach ($this->MenuItems as $mi)
{
if (is_array($mi->Items) && count($mi->Items) > 0)
{
$mi->ClassList[] = "uwt-menu-item-popup";
}
$mi->SubmenuClasslist[] = "uwt-popup";
$menu->Items[] = $mi;
}
$divContent->Controls[] = $menu;
$divContent->Render();
$menu->Render();
echo("</div>");
echo("<div class=\"PreviewContent\">");
echo("<div class=\"Header\">");
if ($this->ClassTitle != "") echo("<span class=\"ClassTitle\">" . $this->ClassTitle . "</span>");
if ($this->Text != "")
{
echo("<span class=\"ObjectTitle\">");
echo("<div class=\"apb-preview-content\">");
echo("<div class=\"apb-header\">");
echo("<h2>");
echo("<span class=\"apb-class-title\">" . $this->ClassTitle . "</span>");
echo("<a href=\"");
if ($this->PostBackURL != "")
{
@ -175,15 +176,16 @@
echo(">");
echo($this->Text);
echo("</a>");
echo("</span>");
}
echo("</h2>");
echo("</div>");
echo("<div class=\"Content\">");
echo("<div class=\"apb-content\">");
}
protected function AfterContent()
{
echo("</div>");
echo("</div>");
echo("<div class=\"uwt-spinner\">&nbsp;</div>");
echo("</div>");
}

View File

@ -0,0 +1,39 @@
<?php
namespace Phast\WebControls;
use Phast\WebControl;
use Phast\WebControlAttribute;
class HiddenField extends WebControl
{
public $Name;
public $Value;
public function __construct()
{
parent::__construct();
$this->Name = null;
$this->Value = null;
$this->TagName = "input";
$this->Attributes[] = new WebControlAttribute("type", "hidden");
$this->HasContent = false;
}
protected function OnInitialize()
{
parent::OnInitialize();
if ($this->Name !== null)
{
$this->Attributes[] = new WebControlAttribute("name", $this->Name);
}
if ($this->Value !== null)
{
$this->Attributes[] = new WebControlAttribute("value", $this->Value);
}
}
}
?>

View File

@ -51,7 +51,7 @@ use Phast\Phast;
$this->ParseChildElements = true;
$this->TagName = "ul";
$this->ClassList[] = "Menu";
$this->ClassList[] = "uwt-menu";
if ($this->Top != null) $this->StyleRules[] = new WebStyleSheetRule("top", $this->Top);
if ($this->Left != null) $this->StyleRules[] = new WebStyleSheetRule("left", $this->Left);
@ -82,11 +82,6 @@ use Phast\Phast;
}
$this->Controls = array();
$liArrow = new HTMLControl("li");
$liArrow->ClassList[] = "Arrow";
$this->Controls[] = $liArrow;
foreach ($this->Items as $menuItem)
{
$this->Controls[] = Menu::CreateMenuItemControl($menuItem);
@ -101,19 +96,20 @@ use Phast\Phast;
$li = new HTMLControl();
$li->Attributes = $menuItem->Attributes;
$li->TagName = "li";
$li->ClassList[] = "Command";
$li->ClassList = $menuItem->ClassList;
$li->ClassList[] = "uwt-menu-item-command";
if ($menuItem->Visible)
{
$li->ClassList[] = "Visible";
$li->ClassList[] = "uwt-visible";
}
if ($menuItem->Selected)
{
$li->ClassList[] = "Selected";
$li->ClassList[] = "uwt-selected";
}
if (count($menuItem->Items) > 0)
{
$li->ClassList[] = "HasChildren";
$li->ClassList[] = "uwt-has-children";
}
$a = new Anchor();
@ -136,13 +132,13 @@ use Phast\Phast;
{
$spanTitle = new HTMLControl();
$spanTitle->TagName = "span";
$spanTitle->ClassList[] = "Title";
$spanTitle->ClassList[] = "uwt-title";
$spanTitle->InnerHTML = $menuItem->Title;
$a->Controls[] = $spanTitle;
$spanDescription = new HTMLControl();
$spanDescription->TagName = "span";
$spanDescription->ClassList[] = "Description";
$spanDescription->ClassList[] = "uwt-description";
$spanDescription->InnerHTML = $menuItem->Description;
$a->Controls[] = $spanDescription;
}
@ -150,7 +146,7 @@ use Phast\Phast;
{
$spanTitle = new HTMLControl();
$spanTitle->TagName = "span";
$spanTitle->ClassList[] = "Title NoDescription";
$spanTitle->ClassList[] = "uwt-title uwt-description-empty";
$spanTitle->InnerHTML = $menuItem->Title;
$a->Controls[] = $spanTitle;
}
@ -160,6 +156,10 @@ use Phast\Phast;
if (count($menuItem->Items) > 0)
{
$menu = new Menu();
foreach ($menuItem->SubmenuClasslist as $className)
{
$menu->ClassList[] = $className;
}
foreach ($menuItem->Items as $item1)
{
$menu->Items[] = $item1;
@ -175,13 +175,13 @@ use Phast\Phast;
$li->TagName = "li";
if ($menuItem->Visible)
{
$li->ClassList[] = "Visible";
$li->ClassList[] = "uwt-visible";
}
$li->ClassList[] = "Header";
$li->ClassList[] = "uwt-header";
$spanHeader = new HTMLControl();
$spanHeader->TagName = "span";
$spanHeader->ClassList[] = "Text";
$spanHeader->ClassList[] = "uwt-text";
$spanHeader->InnerHTML = $menuItem->Title;
$li->Controls[] = $spanHeader;
@ -193,7 +193,7 @@ use Phast\Phast;
$hr->Attributes = $menuItem->Attributes;
if ($menuItem->Visible)
{
$hr->ClassList[] = "Visible";
$hr->ClassList[] = "uwt-visible";
}
$hr->TagName = "hr";
return $hr;
@ -235,6 +235,7 @@ use Phast\Phast;
public $OnClientClick;
public $Selected;
public $Description;
public $SubmenuClasslist;
public function GetItemByID($id)
{
@ -254,6 +255,7 @@ use Phast\Phast;
$this->Description = $description;
if ($items == null) $items = array();
$this->Items = $items;
$this->SubmenuClasslist = array();
}
}
class MenuItemSeparator extends MenuItem

View File

@ -20,6 +20,11 @@
{
public $BreadcrumbItems;
/**
* The Content-Type header.
*/
public string $ContentType;
/**
* True if this Web page should be considered when parsing a path; false otherwise.
* @var boolean
@ -462,6 +467,7 @@
public function __construct()
{
$this->BreadcrumbItems = array();
$this->ContentType = "text/html";
$this->EnableTenantedHosting = true;
$this->Metadata = array();
$this->OpenGraph = new WebOpenGraphSettings();
@ -972,7 +978,7 @@
}
}
header("Content-Type: text/html; charset=utf-8");
header("Content-Type: " . $this->ContentType . "; charset=utf-8");
if (!$this->IsPartial)
{
$tagHTML = new HTMLControl();