From a5eaf1bb01d768b79452ebcb0e250242da8499a7 Mon Sep 17 00:00:00 2001 From: Doug Haber Date: Sun, 2 Mar 2014 19:05:29 -0500 Subject: [PATCH 01/12] Update version to 1.6.1-SNAPSHOT and add compiler params example to README.md --- README.md | 7 +++++-- pom.xml | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f9ad4e2..e39255c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Official LESS CSS Compiler for Java =================================== -**Latest release** 1.3.3 - compatible with less 1.3.3 +**Latest release** 1.6.1-SNAPSHOT - compatible with less 1.6.1 @@ -13,6 +13,9 @@ Look at the simple example below to compile LESS to CSS: // Instantiate the LESS compiler LessCompiler lessCompiler = new LessCompiler(); + + // Instantiate the LESS compiler with some compiler options + LessCompiler lessCompiler = new LessCompiler(Arrays.asList("--relative-urls", "--strict-math=on")); // Compile LESS input string to CSS output string String css = lessCompiler.compile("@color: #4D926F; #header { color: @color; }"); @@ -33,7 +36,7 @@ Maven users should add the library using the following dependency: org.lesscss lesscss - 1.3.3 + 1.6.1-SNAPSHOT (lesscss-java is in the Maven Central repository.) diff --git a/pom.xml b/pom.xml index bdfb300..aa8d3f9 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.lesscss lesscss - 1.5.1-SNAPSHOT + 1.6.1-SNAPSHOT jar Official LESS CSS Compiler for Java Official LESS CSS Compiler for Java From 3ffb30c4e555f9f7bd0d2e66734b23d864b2da55 Mon Sep 17 00:00:00 2001 From: Sandro Boehme Date: Sat, 3 May 2014 18:19:55 +0200 Subject: [PATCH 02/12] fixed #15 and Maven test setup --- .gitignore | 1 + pom.xml | 46 ++++++++++++++++++++- src/main/java/org/lesscss/LessCompiler.java | 9 +--- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index a9ef82c..5fa3062 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ *.iml *.ipr target/ +dependency-reduced-pom.xml diff --git a/pom.xml b/pom.xml index aa8d3f9..7d27223 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,7 @@ org.apache.commons commons-lang3 3.1 + test org.mockito @@ -82,6 +83,18 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 2.17 + + + org.apache.maven.surefire + surefire-junit47 + 2.17 + + + org.apache.maven.plugins maven-compiler-plugin @@ -156,7 +169,38 @@ 1.0 - + + + org.apache.maven.plugins + maven-shade-plugin + 2.2 + + + package + + shade + + + + + org.mozilla:rhino + + + + + org.apache.commons.io + internaldeps.org.apache.commons.io + + + org.slf4j + internaldeps.org.slf4j + + + true + + + + diff --git a/src/main/java/org/lesscss/LessCompiler.java b/src/main/java/org/lesscss/LessCompiler.java index 72783ba..3517328 100644 --- a/src/main/java/org/lesscss/LessCompiler.java +++ b/src/main/java/org/lesscss/LessCompiler.java @@ -19,24 +19,19 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStreamWriter; import java.io.PrintStream; -import java.io.PrintWriter; import java.io.SequenceInputStream; import java.net.URL; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.StringUtils; import org.lesscss.logging.LessLogger; import org.lesscss.logging.LessLoggerFactory; import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; import org.mozilla.javascript.JavaScriptException; -import org.mozilla.javascript.Script; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.tools.shell.Global; @@ -332,7 +327,7 @@ public String compile(String input, String name) throws LessException { tempFile = File.createTempFile("tmp", "less.tmp"); FileUtils.writeStringToFile(tempFile, input, this.encoding); - return compile( tempFile, ""); + return compile( tempFile, name); } catch (IOException e) { throw new LessException(e); @@ -391,7 +386,7 @@ public synchronized String compile(File input, String name) throws LessException logger.debug("Finished compilation of LESS source in %,d ms.", System.currentTimeMillis() - start ); } - return StringUtils.isNotBlank(this.encoding) ? out.toString(encoding) : out.toString(); + return this.encoding != null && !this.encoding.equals("") ? out.toString(encoding) : out.toString(); } catch (Exception e) { if (e instanceof JavaScriptException) { From c434379cfdfea185b9db6a42956c6b83febd6105 Mon Sep 17 00:00:00 2001 From: Sandro Boehme Date: Sat, 3 May 2014 20:35:44 +0200 Subject: [PATCH 03/12] Integrated less 1.7.0 and fixed https://github.com/sandroboehme/lesscss-maven-plugin/issues/1. The next less.js release should have that fixed already. See https://github.com/less/less.js/pull/1956. --- pom.xml | 2 +- src/main/java/org/lesscss/LessCompiler.java | 4 +- ...ess-rhino-1.6.1.js => less-rhino-1.7.0.js} | 749 +++++++++++++----- ...sc-rhino-1.6.1.js => lessc-rhino-1.7.0.js} | 5 +- .../java/integration/LessExceptionIT.java | 9 +- .../java/org/lesscss/LessCompilerTest.java | 6 +- .../resources/compatibility/css/functions.css | 2 +- 7 files changed, 576 insertions(+), 201 deletions(-) rename src/main/resources/META-INF/{less-rhino-1.6.1.js => less-rhino-1.7.0.js} (92%) rename src/main/resources/META-INF/{lessc-rhino-1.6.1.js => lessc-rhino-1.7.0.js} (99%) diff --git a/pom.xml b/pom.xml index 7d27223..17d31c3 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.lesscss lesscss - 1.6.1-SNAPSHOT + 1.7.0.1.0-SNAPSHOT jar Official LESS CSS Compiler for Java Official LESS CSS Compiler for Java diff --git a/src/main/java/org/lesscss/LessCompiler.java b/src/main/java/org/lesscss/LessCompiler.java index 3517328..1ded6fc 100644 --- a/src/main/java/org/lesscss/LessCompiler.java +++ b/src/main/java/org/lesscss/LessCompiler.java @@ -65,8 +65,8 @@ public class LessCompiler { private static final LessLogger logger = LessLoggerFactory.getLogger(LessCompiler.class); - private URL lessJs = LessCompiler.class.getClassLoader().getResource("META-INF/less-rhino-1.6.1.js"); - private URL lesscJs = LessCompiler.class.getClassLoader().getResource("META-INF/lessc-rhino-1.6.1.js"); + private URL lessJs = LessCompiler.class.getClassLoader().getResource("META-INF/less-rhino-1.7.0.js"); + private URL lesscJs = LessCompiler.class.getClassLoader().getResource("META-INF/lessc-rhino-1.7.0.js"); private List customJs = Collections.emptyList(); private List options = Collections.emptyList(); private Boolean compress = null; diff --git a/src/main/resources/META-INF/less-rhino-1.6.1.js b/src/main/resources/META-INF/less-rhino-1.7.0.js similarity index 92% rename from src/main/resources/META-INF/less-rhino-1.6.1.js rename to src/main/resources/META-INF/less-rhino-1.7.0.js index 48e1c56..e95026f 100644 --- a/src/main/resources/META-INF/less-rhino-1.6.1.js +++ b/src/main/resources/META-INF/less-rhino-1.7.0.js @@ -1,4 +1,4 @@ -/* LESS.js v1.6.1 RHINO | Copyright (c) 2009-2014, Alexis Sellier */ +/* LESS.js v1.7.0 RHINO | Copyright (c) 2009-2014, Alexis Sellier */ // // Stub out `require` in rhino @@ -195,8 +195,7 @@ less.Parser = function Parser(env) { var input, // LeSS input string i, // current index in `input` j, // current chunk - temp, // temporarily holds a chunk's state, for backtracking - memo, // temporarily holds `i`, when backtracking + saveStack = [], // holds state for backtracking furthest, // furthest index the parser has gone to chunks, // chunkified input current, // current chunk @@ -226,7 +225,7 @@ less.Parser = function Parser(env) { var fileParsedFunc = function (e, root, fullPath) { parserImports.queue.splice(parserImports.queue.indexOf(path), 1); // Remove the path from the queue - var importedPreviously = fullPath in parserImports.files || fullPath === rootFilename; + var importedPreviously = fullPath === rootFilename; parserImports.files[fullPath] = root; // Store the root @@ -263,8 +262,9 @@ less.Parser = function Parser(env) { } }; - function save() { temp = current; memo = currentPos = i; } - function restore() { current = temp; currentPos = i = memo; } + function save() { currentPos = i; saveStack.push( { current: current, i: i, j: j }); } + function restore() { var state = saveStack.pop(); current = state.current; currentPos = i = state.i; j = state.j; } + function forget() { saveStack.pop(); } function sync() { if (i > currentPos) { @@ -370,6 +370,8 @@ less.Parser = function Parser(env) { if (!current.length && (j < chunks.length - 1)) { current = chunks[++j]; + skipWhitespace(0); // skip space at the beginning of a chunk + return true; // things changed } return oldi !== i || oldj !== j; @@ -524,7 +526,7 @@ less.Parser = function Parser(env) { // Split the input into chunks. chunks = (function (input) { var len = input.length, level = 0, parenLevel = 0, - lastOpening, lastClosing, lastMultiComment, lastMultiCommentEndBrace, + lastOpening, lastOpeningParen, lastMultiComment, lastMultiCommentEndBrace, chunks = [], emitFrom = 0, parserCurrentIndex, currentChunkStartIndex, cc, cc2, matched; @@ -555,17 +557,26 @@ less.Parser = function Parser(env) { switch (cc) { case 40: // ( - parenLevel++; continue; + parenLevel++; + lastOpeningParen = parserCurrentIndex; + continue; case 41: // ) - parenLevel--; continue; + if (--parenLevel < 0) { + return fail("missing opening `(`"); + } + continue; case 59: // ; if (!parenLevel) { emitChunk(); } continue; case 123: // { - level++; lastOpening = parserCurrentIndex; continue; + level++; + lastOpening = parserCurrentIndex; + continue; case 125: // } - level--; lastClosing = parserCurrentIndex; - if (!level) { emitChunk(); } + if (--level < 0) { + return fail("missing opening `{`"); + } + if (!level && !parenLevel) { emitChunk(); } continue; case 92: // \ if (parserCurrentIndex < len - 1) { parserCurrentIndex++; continue; } @@ -609,6 +620,7 @@ less.Parser = function Parser(env) { if (parserCurrentIndex == len - 1) { return fail("missing closing `*/`", currentChunkStartIndex); } + parserCurrentIndex++; } continue; case 42: // *, check for unmatched */ @@ -620,16 +632,13 @@ less.Parser = function Parser(env) { } if (level !== 0) { - if (level > 0) { - if ((lastMultiComment > lastOpening) && (lastMultiCommentEndBrace > lastMultiComment)) { - return fail("missing closing `}` or `*/`", lastOpening); - } else { - return fail("missing closing `}`", lastOpening); - } + if ((lastMultiComment > lastOpening) && (lastMultiCommentEndBrace > lastMultiComment)) { + return fail("missing closing `}` or `*/`", lastOpening); + } else { + return fail("missing closing `}`", lastOpening); } - return fail("missing opening `{`", lastClosing); } else if (parenLevel !== 0) { - return fail((parenLevel > 0) ? "missing closing `)`" : "missing opening `(`"); + return fail("missing closing `)`", lastOpeningParen); } emitChunk(true); @@ -872,7 +881,7 @@ less.Parser = function Parser(env) { while (current) { node = this.extendRule() || mixin.definition() || this.rule() || this.ruleset() || - mixin.call() || this.comment() || this.directive(); + mixin.call() || this.comment() || this.rulesetCall() || this.directive(); if (node) { root.push(node); } else { @@ -880,6 +889,9 @@ less.Parser = function Parser(env) { break; } } + if (peekChar('}')) { + break; + } } return root; @@ -947,7 +959,7 @@ less.Parser = function Parser(env) { keyword: function () { var k; - k = $re(/^[_A-Za-z-][_A-Za-z0-9-]*/); + k = $re(/^%|^[_A-Za-z-][_A-Za-z0-9-]*/); if (k) { var color = tree.Color.fromKeyword(k); if (color) { @@ -1170,6 +1182,19 @@ less.Parser = function Parser(env) { if (input.charAt(i) === '@' && (name = $re(/^(@[\w-]+)\s*:/))) { return name[1]; } }, + // + // The variable part of a variable definition. Used in the `rule` parser + // + // @fink(); + // + rulesetCall: function () { + var name; + + if (input.charAt(i) === '@' && (name = $re(/^(@[\w-]+)\s*\(\s*\)\s*;/))) { + return new tree.RulesetCall(name[1]); + } + }, + // // extend syntax - used to extend selectors // @@ -1243,17 +1268,21 @@ less.Parser = function Parser(env) { if (elements) { elements.push(elem); } else { elements = [ elem ]; } c = $char('>'); } - if ($char('(')) { - args = this.args(true).args; - expectChar(')'); - } - if (parsers.important()) { - important = true; - } + if (elements) { + if ($char('(')) { + args = this.args(true).args; + expectChar(')'); + } - if (elements && ($char(';') || peekChar('}'))) { - return new(tree.mixin.Call)(elements, args, index, env.currentFileInfo, important); + if (parsers.important()) { + important = true; + } + + if (parsers.end()) { + forget(); + return new(tree.mixin.Call)(elements, args, index, env.currentFileInfo, important); + } } restore(); @@ -1264,9 +1293,11 @@ less.Parser = function Parser(env) { expressions = [], argsSemiColon = [], argsComma = [], isSemiColonSeperated, expressionContainsNamed, name, nameLoop, value, arg; + save(); + while (true) { if (isCall) { - arg = parsers.expression(); + arg = parsers.detachedRuleset() || parsers.expression(); } else { parsers.comments(); if (input.charAt(i) === '.' && $re(/^\.{3}/)) { @@ -1294,7 +1325,7 @@ less.Parser = function Parser(env) { if (isCall) { // Variable - if (arg.value.length == 1) { + if (arg.value && arg.value.length == 1) { val = arg.value[0]; } } else { @@ -1309,7 +1340,21 @@ less.Parser = function Parser(env) { } expressionContainsNamed = true; } - value = expect(parsers.expression); + + // we do not support setting a ruleset as a default variable - it doesn't make sense + // However if we do want to add it, there is nothing blocking it, just don't error + // and remove isCall dependency below + value = (isCall && parsers.detachedRuleset()) || parsers.expression(); + + if (!value) { + if (isCall) { + error("could not understand value for named argument"); + } else { + restore(); + returner.args = []; + return returner; + } + } nameLoop = (name = val.name); } else if (!isCall && $re(/^\.{3}/)) { returner.variadic = true; @@ -1354,6 +1399,7 @@ less.Parser = function Parser(env) { } } + forget(); returner.args = isSemiColonSeperated ? argsSemiColon : argsComma; return returner; }, @@ -1394,10 +1440,14 @@ less.Parser = function Parser(env) { variadic = argInfo.variadic; // .mixincall("@{a}"); - // looks a bit like a mixin definition.. so we have to be nice and restore + // looks a bit like a mixin definition.. + // also + // .mixincall(@a: {rule: set;}); + // so we have to be nice and restore if (!$char(')')) { furthest = i; restore(); + return; } parsers.comments(); @@ -1409,10 +1459,13 @@ less.Parser = function Parser(env) { ruleset = parsers.block(); if (ruleset) { + forget(); return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic); } else { restore(); } + } else { + forget(); } } }, @@ -1476,10 +1529,16 @@ less.Parser = function Parser(env) { this.entities.variableCurly(); if (! e) { + save(); if ($char('(')) { if ((v = this.selector()) && $char(')')) { e = new(tree.Paren)(v); + forget(); + } else { + restore(); } + } else { + forget(); } } @@ -1582,6 +1641,22 @@ less.Parser = function Parser(env) { } }, + blockRuleset: function() { + var block = this.block(); + + if (block) { + block = new tree.Ruleset(null, block); + } + return block; + }, + + detachedRuleset: function() { + var blockRuleset = this.blockRuleset(); + if (blockRuleset) { + return new tree.DetachedRuleset(blockRuleset); + } + }, + // // div, .class, body > p {...} // @@ -1612,6 +1687,7 @@ less.Parser = function Parser(env) { } if (selectors && (rules = this.block())) { + forget(); var ruleset = new(tree.Ruleset)(selectors, rules, env.strictImports); if (env.dumpLineNumbers) { ruleset.debugInfo = debugInfo; @@ -1624,27 +1700,38 @@ less.Parser = function Parser(env) { } }, rule: function (tryAnonymous) { - var name, value, c = input.charAt(i), important, merge = false; - save(); + var name, value, startOfRule = i, c = input.charAt(startOfRule), important, merge, isVariable; if (c === '.' || c === '#' || c === '&') { return; } + save(); + name = this.variable() || this.ruleProperty(); if (name) { - // prefer to try to parse first if its a variable or we are compressing - // but always fallback on the other one - value = !tryAnonymous && (env.compress || (name.charAt && (name.charAt(0) === '@'))) ? - (this.value() || this.anonymousValue()) : - (this.anonymousValue() || this.value()); - - important = this.important(); + isVariable = typeof name === "string"; - // a name returned by this.ruleProperty() is always an array of the form: - // ["", "string-1", ..., "string-n", ""] or ["", "string-1", ..., "string-n", "+"] - merge = name.pop && (name.pop() === "+"); + if (isVariable) { + value = this.detachedRuleset(); + } + + if (!value) { + // prefer to try to parse first if its a variable or we are compressing + // but always fallback on the other one + value = !tryAnonymous && (env.compress || isVariable) ? + (this.value() || this.anonymousValue()) : + (this.anonymousValue() || this.value()); + + important = this.important(); + + // a name returned by this.ruleProperty() is always an array of the form: + // [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"] + // where each item is a tree.Keyword or tree.Variable + merge = !isVariable && name.pop().value; + } if (value && this.end()) { - return new (tree.Rule)(name, value, important, merge, memo, env.currentFileInfo); + forget(); + return new (tree.Rule)(name, value, important, merge, startOfRule, env.currentFileInfo); } else { furthest = i; restore(); @@ -1652,6 +1739,8 @@ less.Parser = function Parser(env) { return this.rule(true); } } + } else { + forget(); } }, anonymousValue: function () { @@ -1685,6 +1774,7 @@ less.Parser = function Parser(env) { if (dir && (path = this.entities.quoted() || this.entities.url())) { features = this.mediaFeatures(); if ($char(';')) { + forget(); features = features && new(tree.Value)(features); return new(tree.Import)(path, features, options, index, env.currentFileInfo); } @@ -1801,7 +1891,7 @@ less.Parser = function Parser(env) { // directive: function () { var index = i, name, value, rules, nonVendorSpecificName, - hasBlock, hasIdentifier, hasExpression, identifier; + hasIdentifier, hasExpression, hasUnknown, hasBlock = true; if (input.charAt(i) !== '@') { return; } @@ -1822,9 +1912,8 @@ less.Parser = function Parser(env) { } switch(nonVendorSpecificName) { + /* case "@font-face": - hasBlock = true; - break; case "@viewport": case "@top-left": case "@top-left-corner": @@ -1844,40 +1933,51 @@ less.Parser = function Parser(env) { case "@right-bottom": hasBlock = true; break; - case "@host": - case "@page": - case "@document": - case "@supports": - case "@keyframes": - hasBlock = true; + */ + case "@charset": hasIdentifier = true; + hasBlock = false; break; case "@namespace": hasExpression = true; + hasBlock = false; + break; + case "@keyframes": + hasIdentifier = true; + break; + case "@host": + case "@page": + case "@document": + case "@supports": + hasUnknown = true; break; } if (hasIdentifier) { - identifier = ($re(/^[^{]+/) || '').trim(); - if (identifier) { - name += " " + identifier; + value = this.entity(); + if (!value) { + error("expected " + name + " identifier"); + } + } else if (hasExpression) { + value = this.expression(); + if (!value) { + error("expected " + name + " expression"); + } + } else if (hasUnknown) { + value = ($re(/^[^{;]+/) || '').trim(); + if (value) { + value = new(tree.Anonymous)(value); } } if (hasBlock) { - rules = this.block(); - if (rules) { - return new(tree.Directive)(name, rules, index, env.currentFileInfo); - } - } else { - value = hasExpression ? this.expression() : this.entity(); - if (value && $char(';')) { - var directive = new(tree.Directive)(name, value, index, env.currentFileInfo); - if (env.dumpLineNumbers) { - directive.debugInfo = getDebugInfo(i, input, env); - } - return directive; - } + rules = this.blockRuleset(); + } + + if (rules || (!hasBlock && value && $char(';'))) { + forget(); + return new(tree.Directive)(name, value, rules, index, env.currentFileInfo, + env.dumpLineNumbers ? getDebugInfo(index, input, env) : null); } restore(); @@ -2069,7 +2169,7 @@ less.Parser = function Parser(env) { } }, ruleProperty: function () { - var c = current, name = [], index = [], length = 0; + var c = current, name = [], index = [], length = 0, s, k; function match(re) { var a = re.exec(c); @@ -2083,15 +2183,20 @@ less.Parser = function Parser(env) { match(/^(\*?)/); while (match(/^((?:[\w-]+)|(?:@\{[\w-]+\}))/)); // ! - if ((name.length > 1) && match(/^\s*(\+?)\s*:/)) { + if ((name.length > 1) && match(/^\s*((?:\+_|\+)?)\s*:/)) { // at last, we have the complete match now. move forward, - // convert @{var}s to tree.Variable(s) and return: + // convert name particles to tree objects and return: skipWhitespace(length); - for (var k=0; k < name.length; k++) { - if (name[k].charAt(0) === '@') { - name[k] = new tree.Variable('@' + name[k].slice(2, -1), + if (name[0] === '') { + name.shift(); + index.shift(); + } + for (k = 0; k < name.length; k++) { + s = name[k]; + name[k] = (s.charAt(0) !== '@') + ? new(tree.Keyword)(s) + : new(tree.Variable)('@' + s.slice(2, -1), index[k], env.currentFileInfo); - } } return name; } @@ -2113,6 +2218,7 @@ less.Parser.serializeVars = function(vars) { return s; }; + (function (tree) { tree.functions = { @@ -2210,6 +2316,14 @@ tree.functions = { luma: function (color) { return new(tree.Dimension)(Math.round(color.luma() * color.alpha * 100), '%'); }, + luminance: function (color) { + var luminance = + (0.2126 * color.rgb[0] / 255) + + (0.7152 * color.rgb[1] / 255) + + (0.0722 * color.rgb[2] / 255); + + return new(tree.Dimension)(Math.round(luminance * color.alpha * 100), '%'); + }, saturate: function (color, amount) { // filter: saturate(3.2); // should be kept as is, so check for color @@ -2333,25 +2447,40 @@ tree.functions = { escape: function (str) { return new(tree.Anonymous)(encodeURI(str.value).replace(/=/g, "%3D").replace(/:/g, "%3A").replace(/#/g, "%23").replace(/;/g, "%3B").replace(/\(/g, "%28").replace(/\)/g, "%29")); }, - '%': function (quoted /* arg, arg, ...*/) { + replace: function (string, pattern, replacement, flags) { + var result = string.value; + + result = result.replace(new RegExp(pattern.value, flags ? flags.value : ''), replacement.value); + return new(tree.Quoted)(string.quote || '', result, string.escaped); + }, + '%': function (string /* arg, arg, ...*/) { var args = Array.prototype.slice.call(arguments, 1), - str = quoted.value; + result = string.value; for (var i = 0; i < args.length; i++) { /*jshint loopfunc:true */ - str = str.replace(/%[sda]/i, function(token) { + result = result.replace(/%[sda]/i, function(token) { var value = token.match(/s/i) ? args[i].value : args[i].toCSS(); return token.match(/[A-Z]$/) ? encodeURIComponent(value) : value; }); } - str = str.replace(/%%/g, '%'); - return new(tree.Quoted)('"' + str + '"', str); + result = result.replace(/%%/g, '%'); + return new(tree.Quoted)(string.quote || '', result, string.escaped); }, unit: function (val, unit) { if(!(val instanceof tree.Dimension)) { throw { type: "Argument", message: "the first argument to unit must be a number" + (val instanceof tree.Operation ? ". Have you forgotten parenthesis?" : "") }; } - return new(tree.Dimension)(val.value, unit ? unit.toCSS() : ""); + if (unit) { + if (unit instanceof tree.Keyword) { + unit = unit.value; + } else { + unit = unit.toCSS(); + } + } else { + unit = ""; + } + return new(tree.Dimension)(val.value, unit); }, convert: function (val, unit) { return val.convertTo(unit.value); @@ -2379,28 +2508,34 @@ tree.functions = { _minmax: function (isMin, args) { args = Array.prototype.slice.call(args); switch(args.length) { - case 0: throw { type: "Argument", message: "one or more arguments required" }; - case 1: return args[0]; + case 0: throw { type: "Argument", message: "one or more arguments required" }; } - var i, j, current, currentUnified, referenceUnified, unit, + var i, j, current, currentUnified, referenceUnified, unit, unitStatic, unitClone, order = [], // elems only contains original argument values. values = {}; // key is the unit.toString() for unified tree.Dimension values, // value is the index into the order array. for (i = 0; i < args.length; i++) { current = args[i]; if (!(current instanceof tree.Dimension)) { - order.push(current); + if(Array.isArray(args[i].value)) { + Array.prototype.push.apply(args, Array.prototype.slice.call(args[i].value)); + } continue; } - currentUnified = current.unify(); - unit = currentUnified.unit.toString(); - j = values[unit]; + currentUnified = current.unit.toString() === "" && unitClone !== undefined ? new(tree.Dimension)(current.value, unitClone).unify() : current.unify(); + unit = currentUnified.unit.toString() === "" && unitStatic !== undefined ? unitStatic : currentUnified.unit.toString(); + unitStatic = unit !== "" && unitStatic === undefined || unit !== "" && order[0].unify().unit.toString() === "" ? unit : unitStatic; + unitClone = unit !== "" && unitClone === undefined ? current.unit.toString() : unitClone; + j = values[""] !== undefined && unit !== "" && unit === unitStatic ? values[""] : values[unit]; if (j === undefined) { + if(unitStatic !== undefined && unit !== unitStatic) { + throw{ type: "Argument", message: "incompatible types" }; + } values[unit] = order.length; order.push(current); continue; } - referenceUnified = order[j].unify(); + referenceUnified = order[j].unit.toString() === "" && unitClone !== undefined ? new(tree.Dimension)(order[j].value, unitClone).unify() : order[j].unify(); if ( isMin && currentUnified.value < referenceUnified.value || !isMin && currentUnified.value > referenceUnified.value) { order[j] = current; @@ -2409,8 +2544,7 @@ tree.functions = { if (order.length == 1) { return order[0]; } - args = order.map(function (a) { return a.toCSS(this.env); }) - .join(this.env.compress ? "," : ", "); + args = order.map(function (a) { return a.toCSS(this.env); }).join(this.env.compress ? "," : ", "); return new(tree.Anonymous)((isMin ? "min" : "max") + "(" + args + ")"); }, min: function () { @@ -2419,6 +2553,9 @@ tree.functions = { max: function () { return this._minmax(false, arguments); }, + "get-unit": function (n) { + return new(tree.Anonymous)(n.unit); + }, argb: function (color) { return new(tree.Anonymous)(color.toARGB()); }, @@ -3029,7 +3166,7 @@ tree.debugInfo.asComment = function(ctx) { tree.debugInfo.asMediaQuery = function(ctx) { return '@media -sass-debug-info{filename{font-family:' + - ('file://' + ctx.debugInfo.fileName).replace(/([.:/\\])/g, function (a) { + ('file://' + ctx.debugInfo.fileName).replace(/([.:\/\\])/g, function (a) { if (a == '\\') { a = '\/'; } @@ -3298,7 +3435,17 @@ var transparentKeyword = "transparent"; tree.Color.prototype = { type: "Color", eval: function () { return this; }, - luma: function () { return (0.2126 * this.rgb[0] / 255) + (0.7152 * this.rgb[1] / 255) + (0.0722 * this.rgb[2] / 255); }, + luma: function () { + var r = this.rgb[0] / 255, + g = this.rgb[1] / 255, + b = this.rgb[2] / 255; + + r = (r <= 0.03928) ? r / 12.92 : Math.pow(((r + 0.055) / 1.055), 2.4); + g = (g <= 0.03928) ? g / 12.92 : Math.pow(((g + 0.055) / 1.055), 2.4); + b = (b <= 0.03928) ? b / 12.92 : Math.pow(((b + 0.055) / 1.055), 2.4); + + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + }, genCSS: function (env, output) { output.add(this.toCSS(env)); @@ -3529,6 +3676,27 @@ tree.Condition.prototype = { (function (tree) { +tree.DetachedRuleset = function (ruleset, frames) { + this.ruleset = ruleset; + this.frames = frames; +}; +tree.DetachedRuleset.prototype = { + type: "DetachedRuleset", + accept: function (visitor) { + this.ruleset = visitor.visit(this.ruleset); + }, + eval: function (env) { + var frames = this.frames || env.frames.slice(0); + return new tree.DetachedRuleset(this.ruleset, frames); + }, + callEval: function (env) { + return this.ruleset.eval(this.frames ? new(tree.evalEnv)(env, this.frames.concat(env.frames)) : env); + } +}; +})(require('../tree')); + +(function (tree) { + // // A number with a unit // @@ -3618,17 +3786,27 @@ tree.Dimension.prototype = { compare: function (other) { if (other instanceof tree.Dimension) { - var a = this.unify(), b = other.unify(), - aValue = a.value, bValue = b.value; + var a, b, + aValue, bValue; + + if (this.unit.isEmpty() || other.unit.isEmpty()) { + a = this; + b = other; + } else { + a = this.unify(); + b = other.unify(); + if (a.unit.compare(b.unit) !== 0) { + return -1; + } + } + aValue = a.value; + bValue = b.value; if (bValue > aValue) { return -1; } else if (bValue < aValue) { return 1; } else { - if (!b.unit.isEmpty() && a.unit.compare(b.unit) !== 0) { - return -1; - } return 0; } } else { @@ -3637,7 +3815,7 @@ tree.Dimension.prototype = { }, unify: function () { - return this.convertTo({ length: 'm', duration: 's', angle: 'rad' }); + return this.convertTo({ length: 'px', duration: 's', angle: 'rad' }); }, convertTo: function (conversions) { @@ -3690,6 +3868,7 @@ tree.UnitConversions = { 'cm': 0.01, 'mm': 0.001, 'in': 0.0254, + 'px': 0.0254 / 96, 'pt': 0.0254 / 72, 'pc': 0.0254 / 72 * 12 }, @@ -3843,59 +4022,63 @@ tree.Unit.prototype = { (function (tree) { -tree.Directive = function (name, value, index, currentFileInfo) { - this.name = name; - - if (Array.isArray(value)) { - this.rules = [new(tree.Ruleset)(null, value)]; - this.rules[0].allowImports = true; - } else { - this.value = value; +tree.Directive = function (name, value, rules, index, currentFileInfo, debugInfo) { + this.name = name; + this.value = value; + if (rules) { + this.rules = rules; + this.rules.allowImports = true; } this.index = index; this.currentFileInfo = currentFileInfo; - + this.debugInfo = debugInfo; }; + tree.Directive.prototype = { type: "Directive", accept: function (visitor) { - if (this.rules) { - this.rules = visitor.visitArray(this.rules); + var value = this.value, rules = this.rules; + if (rules) { + rules = visitor.visit(rules); } - if (this.value) { - this.value = visitor.visit(this.value); + if (value) { + value = visitor.visit(value); } }, genCSS: function (env, output) { + var value = this.value, rules = this.rules; output.add(this.name, this.currentFileInfo, this.index); - if (this.rules) { - tree.outputRuleset(env, output, this.rules); - } else { + if (value) { output.add(' '); - this.value.genCSS(env, output); + value.genCSS(env, output); + } + if (rules) { + tree.outputRuleset(env, output, [rules]); + } else { output.add(';'); } }, toCSS: tree.toCSS, eval: function (env) { - var evaldDirective = this; - if (this.rules) { - env.frames.unshift(this); - evaldDirective = new(tree.Directive)(this.name, null, this.index, this.currentFileInfo); - evaldDirective.rules = [this.rules[0].eval(env)]; - evaldDirective.rules[0].root = true; - env.frames.shift(); + var value = this.value, rules = this.rules; + if (value) { + value = value.eval(env); + } + if (rules) { + rules = rules.eval(env); + rules.root = true; } - return evaldDirective; + return new(tree.Directive)(this.name, value, rules, + this.index, this.currentFileInfo, this.debugInfo); }, - variable: function (name) { return tree.Ruleset.prototype.variable.call(this.rules[0], name); }, - find: function () { return tree.Ruleset.prototype.find.apply(this.rules[0], arguments); }, - rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.rules[0]); }, + variable: function (name) { if (this.rules) return tree.Ruleset.prototype.variable.call(this.rules, name); }, + find: function () { if (this.rules) return tree.Ruleset.prototype.find.apply(this.rules, arguments); }, + rulesets: function () { if (this.rules) return tree.Ruleset.prototype.rulesets.apply(this.rules); }, markReferenced: function () { var i, rules; this.isReferenced = true; if (this.rules) { - rules = this.rules[0].rules; + rules = this.rules.rules; for (i = 0; i < rules.length; i++) { if (rules[i].markReferenced) { rules[i].markReferenced(); @@ -4219,7 +4402,14 @@ tree.Import.prototype = { eval: function (env) { var ruleset, features = this.features && this.features.eval(env); - if (this.skip) { return []; } + if (this.skip) { + if (typeof this.skip === "function") { + this.skip = this.skip(); + } + if (this.skip) { + return []; + } + } if (this.options.inline) { //todo needs to reference css file not import @@ -4309,6 +4499,7 @@ tree.Keyword.prototype = { type: "Keyword", eval: function () { return this; }, genCSS: function (env, output) { + if (this.value === '%') { throw { type: "Syntax", message: "Invalid % without number" }; } output.add(this.value); }, toCSS: tree.toCSS, @@ -4395,11 +4586,14 @@ tree.Media.prototype = { find: function () { return tree.Ruleset.prototype.find.apply(this.rules[0], arguments); }, rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.rules[0]); }, emptySelectors: function() { - var el = new(tree.Element)('', '&', this.index, this.currentFileInfo); - return [new(tree.Selector)([el], null, null, this.index, this.currentFileInfo)]; + var el = new(tree.Element)('', '&', this.index, this.currentFileInfo), + sels = [new(tree.Selector)([el], null, null, this.index, this.currentFileInfo)]; + sels[0].mediaEmpty = true; + return sels; }, markReferenced: function () { var i, rules = this.rules[0].rules; + this.rules[0].markReferenced(); this.isReferenced = true; for (i = 0; i < rules.length; i++) { if (rules[i].markReferenced) { @@ -4473,6 +4667,8 @@ tree.Media.prototype = { } }, bubbleSelectors: function (selectors) { + if (!selectors) + return; this.rules = [new(tree.Ruleset)(selectors.slice(0), [this.rules[0]])]; } }; @@ -4540,8 +4736,8 @@ tree.mixin.Call.prototype = { } if (conditionResult[0] || conditionResult[1]) { if (conditionResult[0] != conditionResult[1]) { - candidate.group = conditionResult[1] - ? defTrue : defFalse; + candidate.group = conditionResult[1] ? + defTrue : defFalse; } candidates.push(candidate); @@ -4584,7 +4780,7 @@ tree.mixin.Call.prototype = { mixin.originalRuleset = mixins[m].originalRuleset || mixins[m]; } Array.prototype.push.apply( - rules, mixin.eval(env, args, this.important).rules); + rules, mixin.evalCall(env, args, this.important).rules); } catch (e) { throw { message: e.message, index: this.index, filename: this.currentFileInfo.filename, stack: e.stack }; } @@ -4631,7 +4827,7 @@ tree.mixin.Call.prototype = { } }; -tree.mixin.Definition = function (name, params, rules, condition, variadic) { +tree.mixin.Definition = function (name, params, rules, condition, variadic, frames) { this.name = name; this.selectors = [new(tree.Selector)([new(tree.Element)(null, name, this.index, this.currentFileInfo)])]; this.params = params; @@ -4645,7 +4841,7 @@ tree.mixin.Definition = function (name, params, rules, condition, variadic) { else { return count; } }, 0); this.parent = tree.Ruleset.prototype; - this.frames = []; + this.frames = frames; }; tree.mixin.Definition.prototype = { type: "MixinDefinition", @@ -4668,14 +4864,15 @@ tree.mixin.Definition.prototype = { var frame = new(tree.Ruleset)(null, null), varargs, arg, params = this.params.slice(0), - i, j, val, name, isNamedFound, argIndex; + i, j, val, name, isNamedFound, argIndex, argsLength = 0; mixinEnv = new tree.evalEnv(mixinEnv, [frame].concat(mixinEnv.frames)); if (args) { args = args.slice(0); + argsLength = args.length; - for(i = 0; i < args.length; i++) { + for(i = 0; i < argsLength; i++) { arg = args[i]; if (name = (arg && arg.name)) { isNamedFound = false; @@ -4705,9 +4902,9 @@ tree.mixin.Definition.prototype = { arg = args && args[argIndex]; if (name = params[i].name) { - if (params[i].variadic && args) { + if (params[i].variadic) { varargs = []; - for (j = argIndex; j < args.length; j++) { + for (j = argIndex; j < argsLength; j++) { varargs.push(args[j].value.eval(env)); } frame.prependRule(new(tree.Rule)(name, new(tree.Expression)(varargs).eval(env))); @@ -4720,7 +4917,7 @@ tree.mixin.Definition.prototype = { frame.resetCache(); } else { throw { type: 'Runtime', message: "wrong number of arguments for " + this.name + - ' (' + args.length + ' for ' + this.arity + ')' }; + ' (' + argsLength + ' for ' + this.arity + ')' }; } frame.prependRule(new(tree.Rule)(name, val)); @@ -4729,7 +4926,7 @@ tree.mixin.Definition.prototype = { } if (params[i].variadic && args) { - for (j = argIndex; j < args.length; j++) { + for (j = argIndex; j < argsLength; j++) { evaldArguments[j] = args[j].value.eval(env); } } @@ -4738,9 +4935,12 @@ tree.mixin.Definition.prototype = { return frame; }, - eval: function (env, args, important) { + eval: function (env) { + return new tree.mixin.Definition(this.name, this.params, this.rules, this.condition, this.variadic, this.frames || env.frames.slice(0)); + }, + evalCall: function (env, args, important) { var _arguments = [], - mixinFrames = this.frames.concat(env.frames), + mixinFrames = this.frames ? this.frames.concat(env.frames) : env.frames, frame = this.evalParams(env, new(tree.evalEnv)(env, mixinFrames), args, _arguments), rules, ruleset; @@ -4951,7 +5151,7 @@ tree.Quoted.prototype = { tree.Rule = function (name, value, important, merge, index, currentFileInfo, inline) { this.name = name; - this.value = (value instanceof tree.Value) ? value : new(tree.Value)([value]); + this.value = (value instanceof tree.Value || value instanceof tree.Ruleset) ? value : new(tree.Value)([value]); this.important = important ? ' ' + important.trim() : ''; this.merge = merge; this.index = index; @@ -4979,25 +5179,36 @@ tree.Rule.prototype = { }, toCSS: tree.toCSS, eval: function (env) { - var strictMathBypass = false; - var name = this.name.map ? - this.name.map( function(v) { - return v.eval ? v.eval(env).value : v; - }).join('') : this.name; + var strictMathBypass = false, name = this.name, evaldValue; + if (typeof name !== "string") { + // expand 'primitive' name directly to get + // things faster (~10% for benchmark.less): + name = (name.length === 1) + && (name[0] instanceof tree.Keyword) + ? name[0].value : evalName(env, name); + } if (name === "font" && !env.strictMath) { strictMathBypass = true; env.strictMath = true; } try { + evaldValue = this.value.eval(env); + + if (!this.variable && evaldValue.type === "DetachedRuleset") { + throw { message: "Rulesets cannot be evaluated on a property.", + index: this.index, filename: this.currentFileInfo.filename }; + } + return new(tree.Rule)(name, - this.value.eval(env), + evaldValue, this.important, this.merge, this.index, this.currentFileInfo, this.inline); } catch(e) { - if (e.index === undefined) { + if (typeof e.index !== 'number') { e.index = this.index; + e.filename = this.currentFileInfo.filename; } throw e; } @@ -5016,6 +5227,32 @@ tree.Rule.prototype = { } }; +function evalName(env, name) { + var value = "", i, n = name.length, + output = {add: function (s) {value += s;}}; + for (i = 0; i < n; i++) { + name[i].eval(env).genCSS(env, output); + } + return value; +} + +})(require('../tree')); + +(function (tree) { + +tree.RulesetCall = function (variable) { + this.variable = variable; +}; +tree.RulesetCall.prototype = { + type: "RulesetCall", + accept: function (visitor) { + }, + eval: function (env) { + var detachedRuleset = new(tree.Variable)(this.variable).eval(env); + return detachedRuleset.callEval(env); + } +}; + })(require('../tree')); (function (tree) { @@ -5040,7 +5277,8 @@ tree.Ruleset.prototype = { }, eval: function (env) { var thisSelectors = this.selectors, selectors, - selCnt, i, defaultFunc = tree.defaultFunc; + selCnt, selector, i, defaultFunc = tree.defaultFunc, hasOnePassingSelector = false; + if (thisSelectors && (selCnt = thisSelectors.length)) { selectors = []; defaultFunc.error({ @@ -5048,14 +5286,20 @@ tree.Ruleset.prototype = { message: "it is currently only allowed in parametric mixin guards," }); for (i = 0; i < selCnt; i++) { - selectors.push(thisSelectors[i].eval(env)); + selector = thisSelectors[i].eval(env); + selectors.push(selector); + if (selector.evaldCondition) { + hasOnePassingSelector = true; + } } defaultFunc.reset(); + } else { + hasOnePassingSelector = true; } var rules = this.rules ? this.rules.slice(0) : null, ruleset = new(tree.Ruleset)(selectors, rules, this.strictImports), - rule; + rule, subRule; ruleset.originalRuleset = this; ruleset.root = this.root; @@ -5065,6 +5309,10 @@ tree.Ruleset.prototype = { if(this.debugInfo) { ruleset.debugInfo = this.debugInfo; } + + if (!hasOnePassingSelector) { + rules.length = 0; + } // push the current ruleset to the frames stack var envFrames = env.frames; @@ -5086,8 +5334,8 @@ tree.Ruleset.prototype = { // so they can be evaluated like closures when the time comes. var rsRules = ruleset.rules, rsRuleCnt = rsRules ? rsRules.length : 0; for (i = 0; i < rsRuleCnt; i++) { - if (rsRules[i] instanceof tree.mixin.Definition) { - rsRules[i].frames = envFrames.slice(0); + if (rsRules[i] instanceof tree.mixin.Definition || rsRules[i] instanceof tree.DetachedRuleset) { + rsRules[i] = rsRules[i].eval(env); } } @@ -5110,14 +5358,46 @@ tree.Ruleset.prototype = { rsRuleCnt += rules.length - 1; i += rules.length-1; ruleset.resetCache(); + } else if (rsRules[i] instanceof tree.RulesetCall) { + /*jshint loopfunc:true */ + rules = rsRules[i].eval(env).rules.filter(function(r) { + if ((r instanceof tree.Rule) && r.variable) { + // do not pollute the scope at all + return false; + } + return true; + }); + rsRules.splice.apply(rsRules, [i, 1].concat(rules)); + rsRuleCnt += rules.length - 1; + i += rules.length-1; + ruleset.resetCache(); } } // Evaluate everything else - for (i = 0; i < rsRuleCnt; i++) { + for (i = 0; i < rsRules.length; i++) { rule = rsRules[i]; - if (! (rule instanceof tree.mixin.Definition)) { - rsRules[i] = rule.eval ? rule.eval(env) : rule; + if (! (rule instanceof tree.mixin.Definition || rule instanceof tree.DetachedRuleset)) { + rsRules[i] = rule = rule.eval ? rule.eval(env) : rule; + } + } + + // Evaluate everything else + for (i = 0; i < rsRules.length; i++) { + rule = rsRules[i]; + // for rulesets, check if it is a css guard and can be removed + if (rule instanceof tree.Ruleset && rule.selectors && rule.selectors.length === 1) { + // check if it can be folded in (e.g. & where) + if (rule.selectors[0].isJustParentSelector()) { + rsRules.splice(i--, 1); + + for(var j = 0; j < rule.rules.length; j++) { + subRule = rule.rules[j]; + if (!(subRule instanceof tree.Rule) || !subRule.variable) { + rsRules.splice(++i, 0, subRule); + } + } + } } } @@ -5162,8 +5442,12 @@ tree.Ruleset.prototype = { matchArgs: function (args) { return !args || args.length === 0; }, + // lets you call a css selector with a guard matchCondition: function (args, env) { var lastSelector = this.selectors[this.selectors.length-1]; + if (!lastSelector.evaldCondition) { + return false; + } if (lastSelector.condition && !lastSelector.condition.eval( new(tree.evalEnv)(env, @@ -5344,6 +5628,9 @@ tree.Ruleset.prototype = { toCSS: tree.toCSS, markReferenced: function () { + if (!this.selectors) { + return; + } for (var s = 0; s < this.selectors.length; s++) { this.selectors[s].markReferenced(); } @@ -5550,40 +5837,73 @@ tree.Selector.prototype = { }, createDerived: function(elements, extendList, evaldCondition) { evaldCondition = (evaldCondition != null) ? evaldCondition : this.evaldCondition; - var newSelector = new(tree.Selector)(elements, extendList || this.extendList, this.condition, this.index, this.currentFileInfo, this.isReferenced); + var newSelector = new(tree.Selector)(elements, extendList || this.extendList, null, this.index, this.currentFileInfo, this.isReferenced); newSelector.evaldCondition = evaldCondition; + newSelector.mediaEmpty = this.mediaEmpty; return newSelector; }, match: function (other) { var elements = this.elements, len = elements.length, - oelements, olen, i; - - oelements = other.elements.map( function(v) { - return v.combinator.value + (v.value.value || v.value); - }).join("").match(/[,&#\.\w-]([\w-]|(\\.))*/g); - // ^ regexp could be more simple but see test/less/css-escapes.less:17, doh! - - if (!oelements) { - return 0; - } + olen, i; - if (oelements[0] === "&") { - oelements.shift(); - } + other.CacheElements(); - olen = oelements.length; + olen = other._elements.length; if (olen === 0 || len < olen) { return 0; } else { for (i = 0; i < olen; i++) { - if (elements[i].value !== oelements[i]) { + if (elements[i].value !== other._elements[i]) { return 0; } } } + return olen; // return number of matched elements }, + CacheElements: function(){ + var css = '', len, v, i; + + if( !this._elements ){ + + len = this.elements.length; + for(i = 0; i < len; i++){ + + v = this.elements[i]; + css += v.combinator.value; + + if( !v.value.value ){ + css += v.value; + continue; + } + + if( typeof v.value.value !== "string" ){ + css = ''; + break; + } + css += v.value.value; + } + + this._elements = css.match(/[,&#\.\w-]([\w-]|(\\.))*/g); + + if (this._elements) { + if (this._elements[0] === "&") { + this._elements.shift(); + } + + } else { + this._elements = []; + } + + } + }, + isJustParentSelector: function() { + return !this.mediaEmpty && + this.elements.length === 1 && + this.elements[0].value === '&' && + (this.elements[0].combinator.value === ' ' || this.elements[0].combinator.value === ''); + }, eval: function (env) { var evaldCondition = this.condition && this.condition.eval(env), elements = this.elements, extendList = this.extendList; @@ -6054,12 +6374,21 @@ tree.Variable.prototype = { })(require('./tree')); (function (tree) { - tree.importVisitor = function(importer, finish, evalEnv) { + tree.importVisitor = function(importer, finish, evalEnv, onceFileDetectionMap, recursionDetector) { this._visitor = new tree.visitor(this); this._importer = importer; this._finish = finish; this.env = evalEnv || new tree.evalEnv(); this.importCount = 0; + this.onceFileDetectionMap = onceFileDetectionMap || {}; + this.recursionDetector = {}; + if (recursionDetector) { + for(var fullFilename in recursionDetector) { + if (recursionDetector.hasOwnProperty(fullFilename)) { + this.recursionDetector[fullFilename] = true; + } + } + } }; tree.importVisitor.prototype = { @@ -6106,10 +6435,22 @@ tree.Variable.prototype = { env.importMultiple = true; } - this._importer.push(importNode.getPath(), importNode.currentFileInfo, importNode.options, function (e, root, imported, fullPath) { + this._importer.push(importNode.getPath(), importNode.currentFileInfo, importNode.options, function (e, root, importedAtRoot, fullPath) { if (e && !e.filename) { e.index = importNode.index; e.filename = importNode.currentFileInfo.filename; } - if (imported && !env.importMultiple) { importNode.skip = imported; } + if (!env.importMultiple) { + if (importedAtRoot) { + importNode.skip = true; + } else { + importNode.skip = function() { + if (fullPath in importVisitor.onceFileDetectionMap) { + return true; + } + importVisitor.onceFileDetectionMap[fullPath] = true; + return false; + }; + } + } var subFinish = function(e) { importVisitor.importCount--; @@ -6122,8 +6463,11 @@ tree.Variable.prototype = { if (root) { importNode.root = root; importNode.importedFilename = fullPath; - if (!inlineCSS && !importNode.skip) { - new(tree.importVisitor)(importVisitor._importer, subFinish, env) + var duplicateImport = importedAtRoot || fullPath in importVisitor.recursionDetector; + + if (!inlineCSS && (env.importMultiple || !duplicateImport)) { + importVisitor.recursionDetector[fullPath] = true; + new(tree.importVisitor)(importVisitor._importer, subFinish, env, importVisitor.onceFileDetectionMap, importVisitor.recursionDetector) .run(root); return; } @@ -6235,6 +6579,9 @@ tree.Variable.prototype = { }, visitMixinDefinition: function (mixinNode, visitArgs) { + // mixin definitions do not get eval'd - this means they keep state + // so we have to clear that state here so it isn't used if toCSS is called twice + mixinNode.frames = []; return []; }, @@ -6416,14 +6763,36 @@ tree.Variable.prototype = { } Object.keys(groups).map(function (k) { + + function toExpression(values) { + return new (tree.Expression)(values.map(function (p) { + return p.value; + })); + } + + function toValue(values) { + return new (tree.Value)(values.map(function (p) { + return p; + })); + } + parts = groups[k]; if (parts.length > 1) { rule = parts[0]; - - rule.value = new (tree.Value)(parts.map(function (p) { - return p.value; - })); + var spacedGroups = []; + var lastSpacedGroup = []; + parts.map(function (p) { + if (p.merge==="+") { + if (lastSpacedGroup.length > 0) { + spacedGroups.push(toExpression(lastSpacedGroup)); + } + lastSpacedGroup = []; + } + lastSpacedGroup.push(p); + }); + spacedGroups.push(toExpression(lastSpacedGroup)); + rule.value = toValue(spacedGroups); } }); } diff --git a/src/main/resources/META-INF/lessc-rhino-1.6.1.js b/src/main/resources/META-INF/lessc-rhino-1.7.0.js similarity index 99% rename from src/main/resources/META-INF/lessc-rhino-1.6.1.js rename to src/main/resources/META-INF/lessc-rhino-1.7.0.js index 69a883b..d8c7187 100644 --- a/src/main/resources/META-INF/lessc-rhino-1.6.1.js +++ b/src/main/resources/META-INF/lessc-rhino-1.7.0.js @@ -1,4 +1,4 @@ -/* LESS.js v1.6.1 RHINO | Copyright (c) 2009-2014, Alexis Sellier */ +/* LESS.js v1.7.0 RHINO | Copyright (c) 2009-2014, Alexis Sellier */ /*global name:true, less, loadStyleSheet, os */ @@ -57,7 +57,8 @@ function formatError(ctx, options) { function writeError(ctx, options) { options = options || {}; if (options.silent) { return; } - print(formatError(ctx, options)); + var message = formatError(ctx, options); + throw new Error(message); } function loadStyleSheet(sheet, callback, reload, remaining) { diff --git a/src/test/java/integration/LessExceptionIT.java b/src/test/java/integration/LessExceptionIT.java index b77930f..8cae8ab 100644 --- a/src/test/java/integration/LessExceptionIT.java +++ b/src/test/java/integration/LessExceptionIT.java @@ -17,12 +17,17 @@ import static org.junit.Assert.assertTrue; import org.junit.Test; +import org.lesscss.LessException; public class LessExceptionIT extends AbstractCompileIT { @Test public void testException() throws Exception { - String output = lessCompiler.compile("a { color: @linkColor; }"); - assertTrue(output.startsWith("NameError: variable @linkColor is undefined in")); + try { + lessCompiler.compile("a { color: @linkColor; }"); + } catch (LessException e) { + String exceptionMsg = e.getMessage(); + assertTrue(exceptionMsg!=null && exceptionMsg.startsWith("NameError: variable @linkColor is undefined in")); + } } } diff --git a/src/test/java/org/lesscss/LessCompilerTest.java b/src/test/java/org/lesscss/LessCompilerTest.java index d78a670..77f67df 100644 --- a/src/test/java/org/lesscss/LessCompilerTest.java +++ b/src/test/java/org/lesscss/LessCompilerTest.java @@ -80,7 +80,7 @@ public class LessCompilerTest { @Mock private URL lessJsFile; @Mock private URLConnection lessJsURLConnection; - private static final String lessJsURLToString = "less-rhino-1.6.1.js"; + private static final String lessJsURLToString = "less-rhino-1.7.0.js"; @Mock private InputStream lessJsInputStream; @Mock private InputStreamReader lessJsInputStreamReader; @@ -106,8 +106,8 @@ public void setUp() throws Exception { @Test public void testNewLessCompiler() throws Exception { - assertEquals(LessCompiler.class.getClassLoader().getResource("META-INF/less-rhino-1.6.1.js"), FieldUtils.readField(lessCompiler, "lessJs", true)); - assertEquals(LessCompiler.class.getClassLoader().getResource("META-INF/lessc-rhino-1.6.1.js"), FieldUtils.readField(lessCompiler, "lesscJs", true)); + assertEquals(LessCompiler.class.getClassLoader().getResource("META-INF/less-rhino-1.7.0.js"), FieldUtils.readField(lessCompiler, "lessJs", true)); + assertEquals(LessCompiler.class.getClassLoader().getResource("META-INF/lessc-rhino-1.7.0.js"), FieldUtils.readField(lessCompiler, "lesscJs", true)); assertEquals(Collections.EMPTY_LIST, FieldUtils.readField(lessCompiler, "customJs", true)); } diff --git a/src/test/resources/compatibility/css/functions.css b/src/test/resources/compatibility/css/functions.css index c022e34..56990d0 100644 --- a/src/test/resources/compatibility/css/functions.css +++ b/src/test/resources/compatibility/css/functions.css @@ -17,7 +17,7 @@ format: "rgb(32, 128, 64)"; format-string: "hello world"; format-multiple: "hello earth 2"; - format-url-encode: "red is %23ff0000"; + format-url-encode: 'red is %23ff0000'; eformat: rgb(32, 128, 64); hue: 98; saturation: 12%; From f0a349de6bae14cd8d461446d1d7c87d48706bca Mon Sep 17 00:00:00 2001 From: Sandro Boehme Date: Sun, 11 May 2014 22:53:41 +0200 Subject: [PATCH 04/12] fixed shading for #15 as suggested by @davidmc24 --- pom.xml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 17d31c3..716b7d3 100644 --- a/pom.xml +++ b/pom.xml @@ -189,11 +189,7 @@ org.apache.commons.io - internaldeps.org.apache.commons.io - - - org.slf4j - internaldeps.org.slf4j + org.lesscss.deps.org.apache.commons.io true From 7e740365303dfe8c4ebd49c197ccb7651b4f25cd Mon Sep 17 00:00:00 2001 From: Sandro Boehme Date: Thu, 22 May 2014 18:03:44 +0200 Subject: [PATCH 05/12] changed README.md to use the latest snapshot version --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e39255c..28f2fc8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ Official LESS CSS Compiler for Java =================================== -**Latest release** 1.6.1-SNAPSHOT - compatible with less 1.6.1 - +**Latest release** 1.7.0.1.0-SNAPSHOT - The 1.0 release that is compatible with less 1.7.0 LESS CSS Compiler for Java is a library to compile LESS sources to CSS stylesheets. @@ -36,7 +35,7 @@ Maven users should add the library using the following dependency: org.lesscss lesscss - 1.6.1-SNAPSHOT + 1.7.0.1.0-SNAPSHOT (lesscss-java is in the Maven Central repository.) From a773257f32685d250bf8f90ca5115b7c966d6308 Mon Sep 17 00:00:00 2001 From: Sandro Boehme Date: Thu, 22 May 2014 18:26:01 +0200 Subject: [PATCH 06/12] removed "SNAPSHOT" from the version name in the README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 28f2fc8..4fd2c1d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Official LESS CSS Compiler for Java =================================== -**Latest release** 1.7.0.1.0-SNAPSHOT - The 1.0 release that is compatible with less 1.7.0 +**Latest release** 1.7.0.1.0 - The 1.0 release that is compatible with less 1.7.0 LESS CSS Compiler for Java is a library to compile LESS sources to CSS stylesheets. @@ -35,7 +35,7 @@ Maven users should add the library using the following dependency: org.lesscss lesscss - 1.7.0.1.0-SNAPSHOT + 1.7.0.1.0 (lesscss-java is in the Maven Central repository.) From 2550ee85df4d0c82db1866bdf5b69392d5005123 Mon Sep 17 00:00:00 2001 From: Sandro Boehme Date: Thu, 22 May 2014 18:33:23 +0200 Subject: [PATCH 07/12] [maven-release-plugin] prepare release lesscss-1.7.0.1.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 716b7d3..00330be 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.lesscss lesscss - 1.7.0.1.0-SNAPSHOT + 1.7.0.1.0 jar Official LESS CSS Compiler for Java Official LESS CSS Compiler for Java From 6efeb3b3672aee8b3841f976dc0e2eb2566b02a7 Mon Sep 17 00:00:00 2001 From: Sandro Boehme Date: Thu, 22 May 2014 18:33:29 +0200 Subject: [PATCH 08/12] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 00330be..a97ecd5 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.lesscss lesscss - 1.7.0.1.0 + 1.7.0.1.1-SNAPSHOT jar Official LESS CSS Compiler for Java Official LESS CSS Compiler for Java From d6c394ef55df9b326095fbe5df586214e54d5738 Mon Sep 17 00:00:00 2001 From: "David M. Carr" Date: Tue, 3 Jun 2014 21:33:39 -0400 Subject: [PATCH 09/12] fix shading of slf4j (#51) The previous shading included slf4j-api and slf4j-simple in the shaded jar. slf4j-simple was intended as a runtime dependency, and thus shouldn't have been included. slf4j-api was intended as an optional non-shaded dependency, and thus shouldn't have been included. --- pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pom.xml b/pom.xml index a97ecd5..bf2097d 100644 --- a/pom.xml +++ b/pom.xml @@ -184,6 +184,8 @@ org.mozilla:rhino + org.slf4j:slf4j-api + org.slf4j:slf4j-simple From 7769096b615e467a8d74bd5238aa9ada9d175bb0 Mon Sep 17 00:00:00 2001 From: Sandro Boehme Date: Sun, 8 Jun 2014 18:30:47 +0200 Subject: [PATCH 10/12] Changed Readme for 1.7.0.1.1 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4fd2c1d..1d8440c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Official LESS CSS Compiler for Java =================================== -**Latest release** 1.7.0.1.0 - The 1.0 release that is compatible with less 1.7.0 +**Latest release** 1.7.0.1.1 - The 1.1 release that is compatible with less 1.7.0 LESS CSS Compiler for Java is a library to compile LESS sources to CSS stylesheets. @@ -35,7 +35,7 @@ Maven users should add the library using the following dependency: org.lesscss lesscss - 1.7.0.1.0 + 1.7.0.1.1 (lesscss-java is in the Maven Central repository.) From bafc967271b859dce6873b713e3e5c1f856e3293 Mon Sep 17 00:00:00 2001 From: Sandro Boehme Date: Sun, 8 Jun 2014 18:52:13 +0200 Subject: [PATCH 11/12] [maven-release-plugin] prepare release lesscss-1.7.0.1.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bf2097d..ac3f4a4 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.lesscss lesscss - 1.7.0.1.1-SNAPSHOT + 1.7.0.1.1 jar Official LESS CSS Compiler for Java Official LESS CSS Compiler for Java From a7102a49d891b034b3dbcc6e5ccd74fbb3d2fa05 Mon Sep 17 00:00:00 2001 From: Sandro Boehme Date: Sun, 8 Jun 2014 18:52:19 +0200 Subject: [PATCH 12/12] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ac3f4a4..70324cf 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.lesscss lesscss - 1.7.0.1.1 + 1.7.0.1.2-SNAPSHOT jar Official LESS CSS Compiler for Java Official LESS CSS Compiler for Java