From 60ce17ac379e80c5ca137c559f2fce048516818d Mon Sep 17 00:00:00 2001
From: Andreas Gohr <andi@splitbrain.org>
Date: Wed, 14 Aug 2013 21:33:58 +0200
Subject: [PATCH] updated phpless to 0.4.0

---
 inc/lessc.inc.php | 398 ++++++++++++++++++++++++++++++++++------------
 1 file changed, 296 insertions(+), 102 deletions(-)

diff --git a/inc/lessc.inc.php b/inc/lessc.inc.php
index 5c81ad2a9..0699de52f 100644
--- a/inc/lessc.inc.php
+++ b/inc/lessc.inc.php
@@ -1,7 +1,7 @@
 <?php
 
 /**
- * lessphp v0.3.9
+ * lessphp v0.4.0
  * http://leafo.net/lessphp
  *
  * LESS css compiler, adapted from http://lesscss.org
@@ -38,7 +38,7 @@
  * handling things like indentation.
  */
 class lessc {
-	static public $VERSION = "v0.3.9";
+	static public $VERSION = "v0.4.0";
 	static protected $TRUE = array("keyword", "true");
 	static protected $FALSE = array("keyword", "false");
 
@@ -55,6 +55,8 @@ class lessc {
 
 	protected $numberPrecision = null;
 
+	protected $allParsedFiles = array();
+
 	// set to the parser that generated the current line when compiling
 	// so we know how to create error messages
 	protected $sourceParser = null;
@@ -103,12 +105,17 @@ class lessc {
 		if (substr_compare($url, '.css', -4, 4) === 0) return false;
 
 		$realPath = $this->findImport($url);
+
 		if ($realPath === null) return false;
 
 		if ($this->importDisabled) {
 			return array(false, "/* import disabled */");
 		}
 
+		if (isset($this->allParsedFiles[realpath($realPath)])) {
+			return array(false, null);
+		}
+
 		$this->addParsedFile($realPath);
 		$parser = $this->makeParser($realPath);
 		$root = $parser->parse(file_get_contents($realPath));
@@ -276,6 +283,8 @@ class lessc {
 		foreach ($this->sortProps($block->props) as $prop) {
 			$this->compileProp($prop, $block, $out);
 		}
+
+		$out->lines = array_values(array_unique($out->lines));
 	}
 
 	protected function sortProps($props, $split = false) {
@@ -450,7 +459,7 @@ class lessc {
 		return $left == $right;
 	}
 
-	protected function patternMatch($block, $callingArgs) {
+	protected function patternMatch($block, $orderedArgs, $keywordArgs) {
 		// match the guards if it has them
 		// any one of the groups must have all its guards pass for a match
 		if (!empty($block->guards)) {
@@ -458,7 +467,7 @@ class lessc {
 			foreach ($block->guards as $guardGroup) {
 				foreach ($guardGroup as $guard) {
 					$this->pushEnv();
-					$this->zipSetArgs($block->args, $callingArgs);
+					$this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
 
 					$negate = false;
 					if ($guard[0] == "negate") {
@@ -487,24 +496,34 @@ class lessc {
 			}
 		}
 
-		$numCalling = count($callingArgs);
-
 		if (empty($block->args)) {
-			return $block->isVararg || $numCalling == 0;
+			return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
+		}
+
+		$remainingArgs = $block->args;
+		if ($keywordArgs) {
+			$remainingArgs = array();
+			foreach ($block->args as $arg) {
+				if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) {
+					continue;
+				}
+
+				$remainingArgs[] = $arg;
+			}
 		}
 
 		$i = -1; // no args
 		// try to match by arity or by argument literal
-		foreach ($block->args as $i => $arg) {
+		foreach ($remainingArgs as $i => $arg) {
 			switch ($arg[0]) {
 			case "lit":
-				if (empty($callingArgs[$i]) || !$this->eq($arg[1], $callingArgs[$i])) {
+				if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) {
 					return false;
 				}
 				break;
 			case "arg":
 				// no arg and no default value
-				if (!isset($callingArgs[$i]) && !isset($arg[2])) {
+				if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
 					return false;
 				}
 				break;
@@ -519,14 +538,19 @@ class lessc {
 		} else {
 			$numMatched = $i + 1;
 			// greater than becuase default values always match
-			return $numMatched >= $numCalling;
+			return $numMatched >= count($orderedArgs);
 		}
 	}
 
-	protected function patternMatchAll($blocks, $callingArgs) {
+	protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) {
 		$matches = null;
 		foreach ($blocks as $block) {
-			if ($this->patternMatch($block, $callingArgs)) {
+			// skip seen blocks that don't have arguments
+			if (isset($skip[$block->id]) && !isset($block->args)) {
+				continue;
+			}
+
+			if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
 				$matches[] = $block;
 			}
 		}
@@ -535,7 +559,7 @@ class lessc {
 	}
 
 	// attempt to find blocks matched by path and args
-	protected function findBlocks($searchIn, $path, $args, $seen=array()) {
+	protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) {
 		if ($searchIn == null) return null;
 		if (isset($seen[$searchIn->id])) return null;
 		$seen[$searchIn->id] = true;
@@ -545,7 +569,7 @@ class lessc {
 		if (isset($searchIn->children[$name])) {
 			$blocks = $searchIn->children[$name];
 			if (count($path) == 1) {
-				$matches = $this->patternMatchAll($blocks, $args);
+				$matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
 				if (!empty($matches)) {
 					// This will return all blocks that match in the closest
 					// scope that has any matching block, like lessjs
@@ -555,7 +579,7 @@ class lessc {
 				$matches = array();
 				foreach ($blocks as $subBlock) {
 					$subMatches = $this->findBlocks($subBlock,
-						array_slice($path, 1), $args, $seen);
+						array_slice($path, 1), $orderedArgs, $keywordArgs, $seen);
 
 					if (!is_null($subMatches)) {
 						foreach ($subMatches as $sm) {
@@ -567,39 +591,51 @@ class lessc {
 				return count($matches) > 0 ? $matches : null;
 			}
 		}
-
 		if ($searchIn->parent === $searchIn) return null;
-		return $this->findBlocks($searchIn->parent, $path, $args, $seen);
+		return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
 	}
 
 	// sets all argument names in $args to either the default value
 	// or the one passed in through $values
-	protected function zipSetArgs($args, $values) {
-		$i = 0;
+	protected function zipSetArgs($args, $orderedValues, $keywordValues) {
 		$assignedValues = array();
-		foreach ($args as $a) {
+
+		$i = 0;
+		foreach ($args as  $a) {
 			if ($a[0] == "arg") {
-				if ($i < count($values) && !is_null($values[$i])) {
-					$value = $values[$i];
+				if (isset($keywordValues[$a[1]])) {
+					// has keyword arg
+					$value = $keywordValues[$a[1]];
+				} elseif (isset($orderedValues[$i])) {
+					// has ordered arg
+					$value = $orderedValues[$i];
+					$i++;
 				} elseif (isset($a[2])) {
+					// has default value
 					$value = $a[2];
-				} else $value = null;
+				} else {
+					$this->throwError("Failed to assign arg " . $a[1]);
+					$value = null; // :(
+				}
 
 				$value = $this->reduce($value);
 				$this->set($a[1], $value);
 				$assignedValues[] = $value;
+			} else {
+				// a lit
+				$i++;
 			}
-			$i++;
 		}
 
 		// check for a rest
 		$last = end($args);
 		if ($last[0] == "rest") {
-			$rest = array_slice($values, count($args) - 1);
+			$rest = array_slice($orderedValues, count($args) - 1);
 			$this->set($last[1], $this->reduce(array("list", " ", $rest)));
 		}
 
-		$this->env->arguments = $assignedValues;
+		// wow is this the only true use of PHP's + operator for arrays?
+		$this->env->arguments = $assignedValues + $orderedValues;
 	}
 
 	// compile a prop and update $lines or $blocks appropriately
@@ -624,8 +660,28 @@ class lessc {
 		case 'mixin':
 			list(, $path, $args, $suffix) = $prop;
 
-			$args = array_map(array($this, "reduce"), (array)$args);
-			$mixins = $this->findBlocks($block, $path, $args);
+			$orderedArgs = array();
+			$keywordArgs = array();
+			foreach ((array)$args as $arg) {
+				$argval = null;
+				switch ($arg[0]) {
+				case "arg":
+					if (!isset($arg[2])) {
+						$orderedArgs[] = $this->reduce(array("variable", $arg[1]));
+					} else {
+						$keywordArgs[$arg[1]] = $this->reduce($arg[2]);
+					}
+					break;
+
+				case "lit":
+					$orderedArgs[] = $this->reduce($arg[1]);
+					break;
+				default:
+					$this->throwError("Unknown arg type: " . $arg[0]);
+				}
+			}
+
+			$mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
 
 			if ($mixins === null) {
 				// fwrite(STDERR,"failed to find block: ".implode(" > ", $path)."\n");
@@ -633,7 +689,7 @@ class lessc {
 			}
 
 			foreach ($mixins as $mixin) {
-				if ($mixin === $block && !$args) {
+				if ($mixin === $block && !$orderedArgs) {
 					continue;
 				}
 
@@ -648,7 +704,7 @@ class lessc {
 				if (isset($mixin->args)) {
 					$haveArgs = true;
 					$this->pushEnv();
-					$this->zipSetArgs($mixin->args, $args);
+					$this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs);
 				}
 
 				$oldParent = $mixin->parent;
@@ -705,7 +761,9 @@ class lessc {
 			list(,$importId) = $prop;
 			$import = $this->env->imports[$importId];
 			if ($import[0] === false) {
-				$out->lines[] = $import[1];
+				if (isset($import[1])) {
+					$out->lines[] = $import[1];
+				}
 			} else {
 				list(, $bottom, $parser, $importDir) = $import;
 				$this->compileImportedProps($bottom, $block, $out, $parser, $importDir);
@@ -793,6 +851,60 @@ class lessc {
 		}
 	}
 
+	protected function lib_pow($args) {
+		list($base, $exp) = $this->assertArgs($args, 2, "pow");
+		return pow($this->assertNumber($base), $this->assertNumber($exp));
+	}
+
+	protected function lib_pi() {
+		return pi();
+	}
+
+	protected function lib_mod($args) {
+		list($a, $b) = $this->assertArgs($args, 2, "mod");
+		return $this->assertNumber($a) % $this->assertNumber($b);
+	}
+
+	protected function lib_tan($num) {
+		return tan($this->assertNumber($num));
+	}
+
+	protected function lib_sin($num) {
+		return sin($this->assertNumber($num));
+	}
+
+	protected function lib_cos($num) {
+		return cos($this->assertNumber($num));
+	}
+
+	protected function lib_atan($num) {
+		$num = atan($this->assertNumber($num));
+		return array("number", $num, "rad");
+	}
+
+	protected function lib_asin($num) {
+		$num = asin($this->assertNumber($num));
+		return array("number", $num, "rad");
+	}
+
+	protected function lib_acos($num) {
+		$num = acos($this->assertNumber($num));
+		return array("number", $num, "rad");
+	}
+
+	protected function lib_sqrt($num) {
+		return sqrt($this->assertNumber($num));
+	}
+
+	protected function lib_extract($value) {
+		list($list, $idx) = $this->assertArgs($value, 2, "extract");
+		$idx = $this->assertNumber($idx);
+		// 1 indexed
+		if ($list[0] == "list" && isset($list[2][$idx - 1])) {
+			return $list[2][$idx - 1];
+		}
+	}
+
 	protected function lib_isnumber($value) {
 		return $this->toBool($value[0] == "number");
 	}
@@ -1085,6 +1197,25 @@ class lessc {
 		$this->throwError($error);
 	}
 
+	protected function assertArgs($value, $expectedArgs, $name="") {
+		if ($expectedArgs == 1) {
+			return $value;
+		} else {
+			if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list");
+			$values = $value[2];
+			$numValues = count($values);
+			if ($expectedArgs != $numValues) {
+				if ($name) {
+					$name = $name . ": ";
+				}
+
+				$this->throwError("${name}expecting $expectedArgs arguments, got $numValues");
+			}
+
+			return $values;
+		}
+	}
+
 	protected function toHSL($color) {
 		if ($color[0] == 'hsl') return $color;
 
@@ -1229,6 +1360,10 @@ class lessc {
 			$var = $this->compileValue($reduced);
 			$res = $this->reduce(array("variable", $this->vPrefix . $var));
 
+			if ($res[0] == "raw_color") {
+				$res = $this->coerceColor($res);
+			}
+
 			if (empty($value[2])) $res = $this->lib_e($res);
 
 			return $res;
@@ -1690,7 +1825,6 @@ class lessc {
 		$this->importDir = (array)$this->importDir;
 		$this->importDir[] = $pi['dirname'].'/';
 
-		$this->allParsedFiles = array();
 		$this->addParsedFile($fname);
 
 		$out = $this->compile(file_get_contents($fname), $fname);
@@ -2313,7 +2447,7 @@ class lessc_parser {
 
 		// mixin
 		if ($this->mixinTags($tags) &&
-			($this->argumentValues($argv) || true) &&
+			($this->argumentDef($argv, $isVararg) || true) &&
 			($this->keyword($suffix) || true) && $this->end())
 		{
 			$tags = $this->fixTags($tags);
@@ -2781,38 +2915,18 @@ class lessc_parser {
 		return false;
 	}
 
-	// consume a list of property values delimited by ; and wrapped in ()
-	protected function argumentValues(&$args, $delim = ',') {
-		$s = $this->seek();
-		if (!$this->literal('(')) return false;
-
-		$values = array();
-		while (true) {
-			if ($this->expressionList($value)) $values[] = $value;
-			if (!$this->literal($delim)) break;
-			else {
-				if ($value == null) $values[] = null;
-				$value = null;
-			}
-		}
-
-		if (!$this->literal(')')) {
-			$this->seek($s);
-			return false;
-		}
-
-		$args = $values;
-		return true;
-	}
-
 	// consume an argument definition list surrounded by ()
 	// each argument is a variable name with optional value
 	// or at the end a ... or a variable named followed by ...
-	protected function argumentDef(&$args, &$isVararg, $delim = ',') {
+	// arguments are separated by , unless a ; is in the list, then ; is the
+	// delimiter.
+	protected function argumentDef(&$args, &$isVararg) {
 		$s = $this->seek();
 		if (!$this->literal('(')) return false;
 
 		$values = array();
+		$delim = ",";
+		$method = "expressionList";
 
 		$isVararg = false;
 		while (true) {
@@ -2821,28 +2935,77 @@ class lessc_parser {
 				break;
 			}
 
-			if ($this->variable($vname)) {
-				$arg = array("arg", $vname);
-				$ss = $this->seek();
-				if ($this->assign() && $this->expressionList($value)) {
-					$arg[] = $value;
-				} else {
-					$this->seek($ss);
-					if ($this->literal("...")) {
-						$arg[0] = "rest";
-						$isVararg = true;
+			if ($this->$method($value)) {
+				if ($value[0] == "variable") {
+					$arg = array("arg", $value[1]);
+					$ss = $this->seek();
+
+					if ($this->assign() && $this->$method($rhs)) {
+						$arg[] = $rhs;
+					} else {
+						$this->seek($ss);
+						if ($this->literal("...")) {
+							$arg[0] = "rest";
+							$isVararg = true;
+						}
 					}
+
+					$values[] = $arg;
+					if ($isVararg) break;
+					continue;
+				} else {
+					$values[] = array("lit", $value);
 				}
-				$values[] = $arg;
-				if ($isVararg) break;
-				continue;
 			}
 
-			if ($this->value($literal)) {
-				$values[] = array("lit", $literal);
-			}
 
-			if (!$this->literal($delim)) break;
+			if (!$this->literal($delim)) {
+				if ($delim == "," && $this->literal(";")) {
+					// found new delim, convert existing args
+					$delim = ";";
+					$method = "propertyValue";
+
+					// transform arg list
+					if (isset($values[1])) { // 2 items
+						$newList = array();
+						foreach ($values as $i => $arg) {
+							switch($arg[0]) {
+							case "arg":
+								if ($i) {
+									$this->throwError("Cannot mix ; and , as delimiter types");
+								}
+								$newList[] = $arg[2];
+								break;
+							case "lit":
+								$newList[] = $arg[1];
+								break;
+							case "rest":
+								$this->throwError("Unexpected rest before semicolon");
+							}
+						}
+
+						$newList = array("list", ", ", $newList);
+
+						switch ($values[0][0]) {
+						case "arg":
+							$newArg = array("arg", $values[0][1], $newList);
+							break;
+						case "lit":
+							$newArg = array("lit", $newList);
+							break;
+						}
+
+					} elseif ($values) { // 1 item
+						$newArg = $values[0];
+					}
+
+					if ($newArg) {
+						$values = array($newArg);
+					}
+				} else {
+					break;
+				}
+			}
 		}
 
 		if (!$this->literal(')')) {
@@ -2884,32 +3047,69 @@ class lessc_parser {
 	}
 
 	// a bracketed value (contained within in a tag definition)
-	protected function tagBracket(&$value) {
+	protected function tagBracket(&$parts, &$hasExpression) {
 		// speed shortcut
 		if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") {
 			return false;
 		}
 
 		$s = $this->seek();
-		if ($this->literal('[') && $this->to(']', $c, true) && $this->literal(']', false)) {
-			$value = '['.$c.']';
-			// whitespace?
-			if ($this->whitespace()) $value .= " ";
 
-			// escape parent selector, (yuck)
-			$value = str_replace($this->lessc->parentSelector, "$&$", $value);
-			return true;
-		}
+		$hasInterpolation = false;
 
-		$this->seek($s);
-		return false;
-	}
+		if ($this->literal("[", false)) {
+			$attrParts = array("[");
+			// keyword, string, operator
+			while (true) {
+				if ($this->literal("]", false)) {
+					$this->count--;
+					break; // get out early
+				}
 
-	protected function tagExpression(&$value) {
-		$s = $this->seek();
-		if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) {
-			$value = array('exp', $exp);
-			return true;
+				if ($this->match('\s+', $m)) {
+					$attrParts[] = " ";
+					continue;
+				}
+				if ($this->string($str)) {
+					// escape parent selector, (yuck)
+					foreach ($str[2] as &$chunk) {
+						$chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk);
+					}
+
+					$attrParts[] = $str;
+					$hasInterpolation = true;
+					continue;
+				}
+
+				if ($this->keyword($word)) {
+					$attrParts[] = $word;
+					continue;
+				}
+
+				if ($this->interpolation($inter, false)) {
+					$attrParts[] = $inter;
+					$hasInterpolation = true;
+					continue;
+				}
+
+				// operator, handles attr namespace too
+				if ($this->match('[|-~\$\*\^=]+', $m)) {
+					$attrParts[] = $m[0];
+					continue;
+				}
+
+				break;
+			}
+
+			if ($this->literal("]", false)) {
+				$attrParts[] = "]";
+				foreach ($attrParts as $part) {
+					$parts[] = $part;
+				}
+				$hasExpression = $hasExpression || $hasInterpolation;
+				return true;
+			}
+			$this->seek($s);
 		}
 
 		$this->seek($s);
@@ -2925,13 +3125,9 @@ class lessc_parser {
 
 		$s = $this->seek();
 
-		if (!$simple && $this->tagExpression($tag)) {
-			return true;
-		}
-
 		$hasExpression = false;
 		$parts = array();
-		while ($this->tagBracket($first)) $parts[] = $first;
+		while ($this->tagBracket($parts, $hasExpression));
 
 		$oldWhite = $this->eatWhiteDefault;
 		$this->eatWhiteDefault = false;
@@ -2941,9 +3137,7 @@ class lessc_parser {
 				$parts[] = $m[1];
 				if ($simple) break;
 
-				while ($this->tagBracket($brack)) {
-					$parts[] = $brack;
-				}
+				while ($this->tagBracket($parts, $hasExpression));
 				continue;
 			}
 
@@ -3334,7 +3528,7 @@ class lessc_parser {
 				break;
 			case '"':
 			case "'":
-				if (preg_match('/'.$min[0].'.*?'.$min[0].'/', $text, $m, 0, $count))
+				if (preg_match('/'.$min[0].'.*?(?<!\\\\)'.$min[0].'/', $text, $m, 0, $count))
 					$count += strlen($m[0]) - 1;
 				break;
 			case '//':
-- 
GitLab