From 7ad9f272a482802da2d43fe83841adbe9bcd8cb4 Mon Sep 17 00:00:00 2001
From: derchris
Date: Sat, 2 Feb 2019 15:35:30 +0100
Subject: update less.php to PHP 7.x compatible fork

---
 lib/less.php/Visitor/extendFinder.php   | 114 ++++++++
 lib/less.php/Visitor/import.php         | 139 ++++++++++
 lib/less.php/Visitor/joinSelector.php   |  70 +++++
 lib/less.php/Visitor/processExtends.php | 469 ++++++++++++++++++++++++++++++++
 lib/less.php/Visitor/toCSS.php          | 292 ++++++++++++++++++++
 5 files changed, 1084 insertions(+)
 create mode 100644 lib/less.php/Visitor/extendFinder.php
 create mode 100644 lib/less.php/Visitor/import.php
 create mode 100644 lib/less.php/Visitor/joinSelector.php
 create mode 100644 lib/less.php/Visitor/processExtends.php
 create mode 100644 lib/less.php/Visitor/toCSS.php

(limited to 'lib/less.php/Visitor')

diff --git a/lib/less.php/Visitor/extendFinder.php b/lib/less.php/Visitor/extendFinder.php
new file mode 100644
index 0000000..22b3aac
--- /dev/null
+++ b/lib/less.php/Visitor/extendFinder.php
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * Extend Finder Visitor
+ *
+ * @package Less
+ * @subpackage visitor
+ */
+class Less_Visitor_extendFinder extends Less_Visitor{
+
+	public $contexts = array();
+	public $allExtendsStack;
+	public $foundExtends;
+
+	public function __construct(){
+		$this->contexts = array();
+		$this->allExtendsStack = array(array());
+		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 = array();
+
+		// 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, $extend, &$j){
+		$this->foundExtends = true;
+		$extend = clone $extend;
+		$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 = array();
+		$this->allExtendsStack[] =& $mediaNode->allExtends;
+	}
+
+    public function visitMediaOut(){
+		array_pop($this->allExtendsStack);
+	}
+
+    public function visitDirective( $directiveNode ){
+		$directiveNode->allExtends = array();
+		$this->allExtendsStack[] =& $directiveNode->allExtends;
+	}
+
+    public function visitDirectiveOut(){
+		array_pop($this->allExtendsStack);
+	}
+}
+
+
diff --git a/lib/less.php/Visitor/import.php b/lib/less.php/Visitor/import.php
new file mode 100644
index 0000000..f79a36d
--- /dev/null
+++ b/lib/less.php/Visitor/import.php
@@ -0,0 +1,139 @@
+<?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);
+	}
+
+}
+*/
+
+
diff --git a/lib/less.php/Visitor/joinSelector.php b/lib/less.php/Visitor/joinSelector.php
new file mode 100644
index 0000000..f62af1a
--- /dev/null
+++ b/lib/less.php/Visitor/joinSelector.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * Join Selector Visitor
+ *
+ * @package Less
+ * @subpackage visitor
+ */
+class Less_Visitor_joinSelector extends Less_Visitor{
+
+	public $contexts = array( array() );
+
+	/**
+	 * @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 ){
+
+		$paths = array();
+
+		if( !$rulesetNode->root ){
+			$selectors = array();
+
+			if( $rulesetNode->selectors && $rulesetNode->selectors ){
+				foreach($rulesetNode->selectors as $selector){
+					if( $selector->getIsOutput() ){
+						$selectors[] = $selector;
+					}
+				}
+			}
+
+			if( !$selectors ){
+				$rulesetNode->selectors = null;
+				$rulesetNode->rules = null;
+			}else{
+				$context = end($this->contexts); //$context = $this->contexts[ count($this->contexts) - 1];
+				$paths = $rulesetNode->joinSelectors( $context, $selectors);
+			}
+
+			$rulesetNode->paths = $paths;
+		}
+
+		$this->contexts[] = $paths; //different from less.js. Placed after joinSelectors() so that $this->contexts will get correct $paths
+	}
+
+    public function visitRulesetOut(){
+		array_pop($this->contexts);
+	}
+
+    public function visitMedia($mediaNode) {
+		$context = end($this->contexts); //$context = $this->contexts[ count($this->contexts) - 1];
+
+		if( !count($context) || (is_object($context[0]) && $context[0]->multiMedia) ){
+			$mediaNode->rules[0]->root = true;
+		}
+	}
+
+}
+
diff --git a/lib/less.php/Visitor/processExtends.php b/lib/less.php/Visitor/processExtends.php
new file mode 100644
index 0000000..bb5f082
--- /dev/null
+++ b/lib/less.php/Visitor/processExtends.php
@@ -0,0 +1,469 @@
+<?php
+
+/**
+ * Process Extends Visitor
+ *
+ * @package Less
+ * @subpackage visitor
+ */
+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 = array();
+		$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 = array();
+
+
+		//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];
+
+				// look for circular references
+				if( in_array($targetExtend->object_id, $extend->parent_ids,true) ){
+					continue;
+				}
+
+				// find a match in the target extends self selector (the bit before :extend)
+				$selectorPath = array( $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 = array($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);
+			}
+		}
+	}
+
+
+
+	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 = array();
+		$potentialMatches_len = 0;
+		$potentialMatch = null;
+		$matches = array();
+
+
+
+		// 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[] = array('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 = array(); // 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;
+			}
+
+			if( in_array($first_el, $hackstackSelector->_oelements) ){
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+
+	/**
+	 * @param integer $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;
+	}
+
+
+	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->value ? $elementValue1->value->value : $elementValue1->value );
+		$elementValue2 = ($elementValue2->value->value ? $elementValue2->value->value : $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 = array();
+		$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, ($match['index'] - $currentSelectorPathElementIndex) ) // last parameter of array_slice is different than the last parameter of javascript's slice
+				, array($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);
+	}
+
+}
\ No newline at end of file
diff --git a/lib/less.php/Visitor/toCSS.php b/lib/less.php/Visitor/toCSS.php
new file mode 100644
index 0000000..8aaca96
--- /dev/null
+++ b/lib/less.php/Visitor/toCSS.php
@@ -0,0 +1,292 @@
+<?php
+
+/**
+ * toCSS Visitor
+ *
+ * @package Less
+ * @subpackage visitor
+ */
+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 array();
+		}
+		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 = array();
+		return array();
+	}
+
+	public function visitExtend(){
+		return array();
+	}
+
+	public function visitComment( $commentNode ){
+		if( $commentNode->isSilent() ){
+			return array();
+		}
+		return $commentNode;
+	}
+
+	public function visitMedia( $mediaNode, &$visitDeeper ){
+		$mediaNode->accept($this);
+		$visitDeeper = false;
+
+		if( !$mediaNode->rules ){
+			return array();
+		}
+		return $mediaNode;
+	}
+
+	public function visitDirective( $directiveNode ){
+		if( isset($directiveNode->currentFileInfo['reference']) && (!property_exists($directiveNode,'isReferenced') || !$directiveNode->isReferenced) ){
+			return array();
+		}
+		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 array();
+			}
+			$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 = array();
+		$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,array($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 array();
+	}
+
+
+	/**
+	 * Helper function for visitRuleset()
+	 *
+	 * @return array
+	 */
+	private function visitRulesetPaths($rulesetNode){
+
+		$paths = array();
+		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 = array();
+		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] = array( $ruleCache[$rule->name]->toCSS() );
+					}
+
+					$ruleCSS = $rule->toCSS();
+					if( array_search($ruleCSS,$ruleList) !== false ){
+						array_splice($rules,$i,1);
+					}else{
+						$ruleList[] = $ruleCSS;
+					}
+				}
+			}
+		}
+	}
+
+	protected function _mergeRules( &$rules ){
+		$groups = array();
+
+		//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] = array();
+				}else{
+					array_splice($rules, $i--, 1);
+					$rules_len--;
+				}
+
+				$groups[$key][] = $rule;
+			}
+		}
+
+
+		foreach($groups as $parts){
+
+			if( count($parts) > 1 ){
+				$rule = $parts[0];
+				$spacedGroups = array();
+				$lastSpacedGroup = array();
+				$parts_mapped = array();
+				foreach($parts as $p){
+					if( $p->merge === '+' ){
+						if( $lastSpacedGroup ){
+							$spacedGroups[] = self::toExpression($lastSpacedGroup);
+						}
+						$lastSpacedGroup = array();
+					}
+					$lastSpacedGroup[] = $p;
+				}
+
+				$spacedGroups[] = self::toExpression($lastSpacedGroup);
+				$rule->value = self::toValue($spacedGroups);
+			}
+		}
+
+	}
+
+	public static function toExpression($values){
+		$mapped = array();
+		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 = array();
+		foreach($values as $p){
+			$mapped[] = $p;
+		}
+		return new Less_Tree_Value($mapped);
+	}
+}
+
-- 
cgit v1.2.3