Updated to 0.4.0
authorSascha Greuel <SoftCreatR@users.noreply.github.com>
Tue, 27 May 2014 18:08:19 +0000 (20:08 +0200)
committerSascha Greuel <SoftCreatR@users.noreply.github.com>
Tue, 27 May 2014 18:08:19 +0000 (20:08 +0200)
Also re-implemented the "Work-around for superfluous brackets" (cba23a28c36273ed11e53bea5c66e6f7f33ed4ea)

wcfsetup/install/files/lib/system/style/lessc.inc.php

index c33ce56ad43cec1a42a6e7f40737d7d1b810a445..478302c1ceaa0e2aee66d31323a5c40ca080a695 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 // @codingStandardsIgnoreFile
 /**
- * lessphp v0.3.8
+ * 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.8";
+       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) {
@@ -327,6 +336,9 @@ class lessc {
                                                $parts[] = "($q[1])";
                                        }
                                        break;
+                               case "variable":
+                                       $parts[] = $this->compileValue($this->reduce($q));
+                               break;
                                }
                        }
 
@@ -434,7 +446,7 @@ class lessc {
                foreach ($selectors as $s) {
                        if (is_array($s)) {
                                list(, $value) = $s;
-                               $out[] = $this->compileValue($this->reduce($value));
+                               $out[] = trim($this->compileValue($this->reduce($value)));
                        } else {
                                $out[] = $s;
                        }
@@ -447,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)) {
@@ -455,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") {
@@ -484,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;
@@ -516,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;
                        }
                }
@@ -532,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;
@@ -542,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
@@ -552,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) {
@@ -564,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
@@ -621,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");
@@ -630,6 +689,10 @@ class lessc {
                        }
 
                        foreach ($mixins as $mixin) {
+                               if ($mixin === $block && !$orderedArgs) {
+                                       continue;
+                               }
+
                                $haveScope = false;
                                if (isset($mixin->parent->scope)) {
                                        $haveScope = true;
@@ -641,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;
@@ -698,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);
@@ -786,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");
        }
@@ -814,6 +933,10 @@ class lessc {
                return $this->toBool($value[0] == "number" && $value[2] == "em");
        }
 
+       protected function lib_isrem($value) {
+               return $this->toBool($value[0] == "number" && $value[2] == "rem");
+       }
+
        protected function lib_rgbahex($color) {
                $color = $this->coerceColor($color);
                if (is_null($color))
@@ -890,6 +1013,16 @@ class lessc {
                return array("number", round($value), $arg[2]);
        }
 
+       protected function lib_unit($arg) {
+               if ($arg[0] == "list") {
+                       list($number, $newUnit) = $arg[2];
+                       return array("number", $this->assertNumber($number),
+                               $this->compileValue($this->lib_e($newUnit)));
+               } else {
+                       return array("number", $this->assertNumber($arg), "");
+               }
+       }
+
        /**
         * Helper function to get arguments for color manipulation functions.
         * takes a list that contains a color like thing and a percentage
@@ -996,19 +1129,24 @@ class lessc {
        }
 
        // mixes two colors by weight
-       // mix(@color1, @color2, @weight);
+       // mix(@color1, @color2, [@weight: 50%]);
        // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method
        protected function lib_mix($args) {
-               if ($args[0] != "list" || count($args[2]) < 3)
+               if ($args[0] != "list" || count($args[2]) < 2)
                        $this->throwError("mix expects (color1, color2, weight)");
 
-               list($first, $second, $weight) = $args[2];
+               list($first, $second) = $args[2];
                $first = $this->assertColor($first);
                $second = $this->assertColor($second);
 
                $first_a = $this->lib_alpha($first);
                $second_a = $this->lib_alpha($second);
-               $weight = $weight[1] / 100.0;
+
+               if (isset($args[2][2])) {
+                       $weight = $args[2][2][1] / 100.0;
+               } else {
+                       $weight = 0.5;
+               }
 
                $w = $weight * 2 - 1;
                $a = $first_a - $second_a;
@@ -1029,6 +1167,25 @@ class lessc {
                return $this->fixColor($new);
        }
 
+       protected function lib_contrast($args) {
+               if ($args[0] != 'list' || count($args[2]) < 3) {
+                       return array(array('color', 0, 0, 0), 0);
+               }
+
+               list($inputColor, $darkColor, $lightColor) = $args[2];
+
+               $inputColor = $this->assertColor($inputColor);
+               $darkColor = $this->assertColor($darkColor);
+               $lightColor = $this->assertColor($lightColor);
+               $hsl = $this->toHSL($inputColor);
+
+               if ($hsl[3] > 50) {
+                       return $darkColor;
+               }
+
+               return $lightColor;
+       }
+
        protected function assertColor($value, $error = "expected color value") {
                $color = $this->coerceColor($value);
                if (is_null($color)) $this->throwError($error);
@@ -1040,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;
 
@@ -1091,7 +1267,7 @@ class lessc {
         * Expects H to be in range of 0 to 360, S and L in 0 to 100
         */
        protected function toRGB($color) {
-               if ($color == 'color') return $color;
+               if ($color[0] == 'color') return $color;
 
                $H = $color[1] / 360;
                $S = $color[2] / 100;
@@ -1179,6 +1355,18 @@ class lessc {
 
        protected function reduce($value, $forExpression = false) {
                switch ($value[0]) {
+               case "interpolate":
+                       $reduced = $this->reduce($value[1]);
+                       $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;
                case "variable":
                        $key = $value[1];
                        if (is_array($key)) {
@@ -1299,8 +1487,12 @@ class lessc {
                        case 'keyword':
                                $name = $value[1];
                                if (isset(self::$cssColors[$name])) {
-                                       list($r, $g, $b) = explode(',', self::$cssColors[$name]);
-                                       return array('color', $r, $g, $b);
+                                       $rgba = explode(',', self::$cssColors[$name]);
+
+                                       if(isset($rgba[3]))
+                                               return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]);
+
+                                       return array('color', $rgba[0], $rgba[1], $rgba[2]);
                                }
                                return null;
                }
@@ -1445,6 +1637,34 @@ class lessc {
                return $this->fixColor($out);
        }
 
+       function lib_red($color){
+               $color = $this->coerceColor($color);
+               if (is_null($color)) {
+                       $this->throwError('color expected for red()');
+               }
+
+               return $color[1];
+       }
+
+       function lib_green($color){
+               $color = $this->coerceColor($color);
+               if (is_null($color)) {
+                       $this->throwError('color expected for green()');
+               }
+
+               return $color[2];
+       }
+
+       function lib_blue($color){
+               $color = $this->coerceColor($color);
+               if (is_null($color)) {
+                       $this->throwError('color expected for blue()');
+               }
+
+               return $color[3];
+       }
+
+
        // operator on two numbers
        protected function op_number_number($op, $left, $right) {
                $unit = empty($left[2]) ? $right[2] : $left[2];
@@ -1605,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);
@@ -1945,6 +2164,7 @@ class lessc {
                'teal' => '0,128,128',
                'thistle' => '216,191,216',
                'tomato' => '255,99,71',
+               'transparent' => '0,0,0,0',
                'turquoise' => '64,224,208',
                'violet' => '238,130,238',
                'wheat' => '245,222,179',
@@ -1988,7 +2208,7 @@ class lessc_parser {
        static protected $supressDivisionProps =
                array('/border-radius$/i', '/^font$/i');
 
-       protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document");
+       protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport");
        protected $lineDirectives = array("charset");
 
        /**
@@ -2227,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);
@@ -2519,6 +2739,9 @@ class lessc_parser {
                        $out = array("mediaExp", $feature);
                        if ($value) $out[] = $value;
                        return true;
+               } elseif ($this->variable($variable)) {
+                       $out = array('variable', $variable);
+                       return true;
                }
 
                $this->seek($s);
@@ -2572,12 +2795,10 @@ class lessc_parser {
                                continue;
                        }
 
-                       if (in_array($tok, $rejectStrs)) {
-                               $count = null;
+                       if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
                                break;
                        }
 
-
                        $content[] = $tok;
                        $this->count+= strlen($tok);
                }
@@ -2652,10 +2873,10 @@ class lessc_parser {
 
                $s = $this->seek();
                if ($this->literal("@{") &&
-                       $this->keyword($var) &&
+                       $this->openString("}", $interp, null, array("'", '"', ";")) &&
                        $this->literal("}", false))
                {
-                       $out = array("variable", $this->lessc->vPrefix . $var);
+                       $out = array("interpolate", $interp);
                        $this->eatWhiteDefault = $oldWhite;
                        if ($this->eatWhiteDefault) $this->whitespace();
                        return true;
@@ -2694,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) {
@@ -2734,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(')')) {
@@ -2797,70 +3047,136 @@ 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);
                return false;
        }
 
-       // a single tag
+       // a space separated list of selectors
        protected function tag(&$tag, $simple = false) {
                if ($simple)
-                       $chars = '^,:;{}\][>\(\) "\'';
+                       $chars = '^@,:;{}\][>\(\) "\'';
                else
-                       $chars = '^,;{}["\'';
+                       $chars = '^@,;{}["\'';
 
-               if (!$simple && $this->tagExpression($tag)) {
-                       return true;
-               }
+               $s = $this->seek();
 
-               $tag = '';
-               while ($this->tagBracket($first)) $tag .= $first;
+               $hasExpression = false;
+               $parts = array();
+               while ($this->tagBracket($parts, $hasExpression));
+
+               $oldWhite = $this->eatWhiteDefault;
+               $this->eatWhiteDefault = false;
 
                while (true) {
                        if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) {
-                               $tag .= $m[1];
+                               $parts[] = $m[1];
                                if ($simple) break;
 
-                               while ($this->tagBracket($brack)) $tag .= $brack;
+                               while ($this->tagBracket($parts, $hasExpression));
                                continue;
-                       } elseif ($this->unit($unit)) { // for keyframes
-                               $tag .= $unit[1] . $unit[2];
+                       }
+
+                       if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") {
+                               if ($this->interpolation($interp)) {
+                                       $hasExpression = true;
+                                       $interp[2] = true; // don't unescape
+                                       $parts[] = $interp;
+                                       continue;
+                               }
+
+                               if ($this->literal("@")) {
+                                       $parts[] = "@";
+                                       continue;
+                               }
+                       }
+
+                       if ($this->unit($unit)) { // for keyframes
+                               $parts[] = $unit[1];
+                               $parts[] = $unit[2];
                                continue;
                        }
+
                        break;
                }
 
+               $this->eatWhiteDefault = $oldWhite;
+               if (!$parts) {
+                       $this->seek($s);
+                       return false;
+               }
 
-               $tag = trim($tag);
-               if ($tag == '') return false;
+               if ($hasExpression) {
+                       $tag = array("exp", array("string", "", $parts));
+               } else {
+                       $tag = trim(implode($parts));
+               }
 
+               $this->whitespace();
                return true;
        }
 
@@ -2948,7 +3264,7 @@ class lessc_parser {
        protected function end() {
                if ($this->literal(';')) {
                        return true;
-               } elseif ($this->count == strlen($this->buffer) || $this->buffer{$this->count} == '}') {
+               } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
                        // if there is end of file or a closing block next then we don't need a ;
                        return true;
                }
@@ -3212,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 '//':