From 6fb31b8632ee381e7ab7f9b7c45c3fe9aea9657e Mon Sep 17 00:00:00 2001 From: Andrey Rublev Date: Mon, 17 Oct 2011 18:03:57 +0700 Subject: [PATCH] Release string interpolation in @import statements. Warning! many api changes! --- Makefile | 2 + benchmark/less-benchmark.js | 39 +-- bin/lessc | 42 ++-- lib/less/browser.js | 38 +-- lib/less/index.js | 40 +-- lib/less/parser.js | 61 ++--- lib/less/rhino.js | 31 +-- lib/less/tree/import.js | 79 +++--- lib/less/tree/ruleset.js | 122 ++++++---- lib/less/when.js | 361 ++++++++++++++++++++++++++++ test/less-test.js | 27 +-- test/less/import.less | 5 +- test/less/import/import-test-b.less | 2 + test/less/import/import-test-c.less | 2 +- 14 files changed, 636 insertions(+), 215 deletions(-) create mode 100644 lib/less/when.js diff --git a/Makefile b/Makefile index 8baea4fa1..bdfa62677 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,7 @@ less: @@echo "(function (window, undefined) {" >> ${DIST} @@cat build/require.js\ build/ecma-5.js\ + ${SRC}/when.js\ ${SRC}/parser.js\ ${SRC}/functions.js\ ${SRC}/tree/*.js\ @@ -41,6 +42,7 @@ rhino: @@touch ${RHINO} @@cat build/require-rhino.js\ build/ecma-5.js\ + ${SRC}/when.js\ ${SRC}/parser.js\ ${SRC}/functions.js\ ${SRC}/tree/*.js\ diff --git a/benchmark/less-benchmark.js b/benchmark/less-benchmark.js index fe142c06c..7bb40da73 100644 --- a/benchmark/less-benchmark.js +++ b/benchmark/less-benchmark.js @@ -17,7 +17,8 @@ fs.readFile(file, 'utf8', function (e, data) { start = new(Date); - new(less.Parser)({ optimization: 2 }).parse(data, function (err, tree) { + try { + var root = new(less.Parser)({ optimization: 2 }).parse(data); end = new(Date); total = end - start; @@ -28,22 +29,24 @@ fs.readFile(file, 'utf8', function (e, data) { data.length / 1024) + " KB\/s)"); start = new(Date); - css = tree.toCSS(); - end = new(Date); - - sys.puts("Generation: " + (end - start) + " ms (" + - parseInt(1000 / (end - start) * - data.length / 1024) + " KB\/s)"); - - total += end - start; - - sys.puts("Total: " + total + "ms (" + - parseInt(1000 / total * data.length / 1024) + " KB/s)"); - - if (err) { - less.writeError(err); - process.exit(3); - } - }); + root.toCSS().then(function(css) { + end = new(Date); + + sys.puts("Generation: " + (end - start) + " ms (" + + parseInt(1000 / (end - start) * + data.length / 1024) + " KB\/s)"); + + total += end - start; + + sys.puts("Total: " + total + "ms (" + + parseInt(1000 / total * data.length / 1024) + " KB/s)"); + }, function(e) { + less.writeError(e); + process.exit(2); + }); + } catch (e) { + less.writeError(e); + process.exit(1); + } }); diff --git a/bin/lessc b/bin/lessc index 1e3c961ff..b08d8184c 100755 --- a/bin/lessc +++ b/bin/lessc @@ -88,29 +88,27 @@ var parseLessFile = function (e, data) { process.exit(1); } - new(less.Parser)({ - paths: [path.dirname(input)].concat(options.paths), - optimization: options.optimization, - filename: input - }).parse(data, function (err, tree) { - if (err) { - less.writeError(err, options); - process.exit(1); - } else { - try { - css = tree.toCSS({ compress: options.compress }); - if (output) { - fd = fs.openSync(output, "w"); - fs.writeSync(fd, css, 0, "utf8"); - } else { - sys.print(css); - } - } catch (e) { - less.writeError(e, options); - process.exit(2); + try { + var root = new(less.Parser)({ + paths: [path.dirname(input)].concat(options.paths), + optimization: options.optimization, + filename: input + }).parse(data); + root.toCSS({ compress: options.compress }).then(function(css) { + if (output) { + fd = fs.openSync(output, "w"); + fs.writeSync(fd, css, 0, "utf8"); + } else { + sys.print(css); } - } - }); + }, function(e) { + less.writeError(e, options); + process.exit(2); + }); + } catch (e) { + less.writeError(e, options); + process.exit(1); + } }; if (input != '-') { diff --git a/lib/less/browser.js b/lib/less/browser.js index cba4c3b61..ebfc1fd5c 100644 --- a/lib/less/browser.js +++ b/lib/less/browser.js @@ -41,7 +41,9 @@ if (less.env === 'development') { if (less.watchMode) { loadStyleSheets(function (root, sheet, env) { if (root) { - createCSS(root.toCSS(), sheet, env.lastModified); + root.toCSS().then(function(css) { + createCSS(css, sheet, env.lastModified); + }); } }); } @@ -78,16 +80,24 @@ less.refresh = function (reload) { var startTime, endTime; startTime = endTime = new(Date); + var d = when.defer(); + loadStyleSheets(function (root, sheet, env) { if (env.local) { log("loading " + sheet.href + " from cache."); + d.resolve(); } else { log("parsed " + sheet.href + " successfully."); - createCSS(root.toCSS(), sheet, env.lastModified); + root.toCSS().then(function(css) { + createCSS(css, sheet, env.lastModified); + d.resolve(); + }); } - log("css for " + sheet.href + " generated in " + (new(Date) - endTime) + 'ms'); - (env.remaining === 0) && log("css generated in " + (new(Date) - startTime) + 'ms'); - endTime = new(Date); + d.promise.then(function() { + log("css for " + sheet.href + " generated in " + (new(Date) - endTime) + 'ms'); + (env.remaining === 0) && log("css generated in " + (new(Date) - startTime) + 'ms'); + endTime = new(Date); + }); }, reload); loadStyles(); @@ -100,8 +110,8 @@ function loadStyles() { var styles = document.getElementsByTagName('style'); for (var i = 0; i < styles.length; i++) { if (styles[i].type.match(typePattern)) { - new(less.Parser)().parse(styles[i].innerHTML || '', function (e, tree) { - var css = tree.toCSS(); + var root = new(less.Parser)().parse(styles[i].innerHTML || ''); + root.toCSS().then(function(css) { var style = styles[i]; try { style.innerHTML = css; @@ -146,19 +156,13 @@ function loadStyleSheet(sheet, callback, reload, remaining) { } else { // Use remote copy (re-parse) try { - new(less.Parser)({ + var root = new(less.Parser)({ optimization: less.optimization, paths: [href.replace(/[\w\.-]+$/, '')], mime: sheet.type - }).parse(data, function (e, root) { - if (e) { return error(e, href) } - try { - callback(root, sheet, { local: false, lastModified: lastModified, remaining: remaining }); - removeNode(document.getElementById('less-error-message:' + extractId(href))); - } catch (e) { - error(e, href); - } - }); + }).parse(data); + callback(root, sheet, { local: false, lastModified: lastModified, remaining: remaining }); + removeNode(document.getElementById('less-error-message:' + extractId(href))); } catch (e) { error(e, href); } diff --git a/lib/less/index.js b/lib/less/index.js index a333f63fd..27a8f6618 100644 --- a/lib/less/index.js +++ b/lib/less/index.js @@ -20,17 +20,26 @@ var less = { ee; if (callback) { - parser.parse(input, function (e, root) { - callback(e, root.toCSS(options)); - }); + var root = parser.parse(input); + root.toCSS(options).then(function(result) { + callback(null, result); + }, function(e) { + callback(e); + }) } else { ee = new(require('events').EventEmitter); process.nextTick(function () { - parser.parse(input, function (e, root) { - if (e) { ee.emit('error', e) } - else { ee.emit('success', root.toCSS(options)) } - }); + try { + var root = parser.parse(input); + root.toCSS(options).then(function(css) { + ee.emit('success', css); + }, function(e) { + ee.emit('error', e); + }) + } catch (e) { + ee.emit('error', e); + } }); return ee; } @@ -39,9 +48,8 @@ var less = { var message = ""; var extract = ctx.extract; var error = []; - var stylize = options.color ? less.stylize : function (str) { return str }; - options = options || {}; + var stylize = options.color ? less.stylize : function (str) { return str }; if (options.silent) { return } @@ -103,13 +111,15 @@ less.Parser.importer = function (file, paths, callback) { fs.readFile(pathname, 'utf-8', function(e, data) { if (e) sys.error(e); - new(less.Parser)({ - paths: [path.dirname(pathname)], - filename: pathname - }).parse(data, function (e, root) { - if (e) less.writeError(e); + try { + var root = new(less.Parser)({ + paths: [path.dirname(pathname)], + filename: pathname + }).parse(data); callback(root); - }); + } catch(e) { + less.writeError(e); + } }); } else { sys.error("file '" + file + "' wasn't found.\n"); diff --git a/lib/less/parser.js b/lib/less/parser.js index 468b97789..2af643920 100644 --- a/lib/less/parser.js +++ b/lib/less/parser.js @@ -1,4 +1,4 @@ -var less, tree; +var less, tree, when; var BROWSER = 0; var NODE = 1; var RHINO = 2; var engine; @@ -13,6 +13,7 @@ if (typeof environment === "object" && ({}).toString.call(environment) === "[obj // Node.js less = exports, tree = require('less/tree'); + when = require('less/when'); engine = NODE; } else { // Browser @@ -67,29 +68,19 @@ less.Parser = function Parser(env) { var that = this; - // This function is called after all files - // have been imported through `@import`. - var finish = function () {}; - var imports = this.imports = { paths: env && env.paths || [], // Search paths, when importing - queue: [], // Files which haven't been imported yet files: {}, // Holds the imported parse trees mime: env && env.mime, // MIME type of .less files push: function (path, callback) { var that = this; - this.queue.push(path); // // Import a file asynchronously // less.Parser.importer(path, this.paths, function (root) { - that.queue.splice(that.queue.indexOf(path), 1); // Remove the path from the queue - that.files[path] = root; // Store the root - + that.files[path] = root; callback(root); - - if (that.queue.length === 0) { finish() } // Call `finish` if we're done importing }, env); } }; @@ -193,9 +184,9 @@ less.Parser = function Parser(env) { imports: imports, // // Parse an input string into an abstract syntax tree, - // call `callback` when done. + // returns tree.Ruleset instance // - parse: function (str, callback) { + parse: function (str) { var root, start, end, zone, line, lines, buff = [], c, error = null; i = j = current = furthest = 0; @@ -312,10 +303,9 @@ less.Parser = function Parser(env) { frames = [new(tree.Ruleset)(null, variables)]; } - try { - var css = evaluate.call(this, { frames: frames }) - .toCSS([], { compress: options.compress || false }); - } catch (e) { + var d = when.defer(); + + var errorHandler = function(e) { lines = input.split('\n'); line = getLine(e.index); @@ -323,7 +313,7 @@ less.Parser = function Parser(env) { n >= 0 && input.charAt(n) !== '\n'; n--) { column++ } - throw { + return { type: e.type, message: e.message, filename: env.filename, @@ -340,12 +330,27 @@ less.Parser = function Parser(env) { ] }; } - if (options.compress) { - return css.replace(/(\s)+/g, "$1"); - } else { - return css; + + try { + evaluate.call(this, { frames: frames }).then(function(evaluated) { + var css = evaluated.toCSS([], { compress: options.compress || false }); + if (options.compress) { + css = css.replace(/(\s)+/g, "$1"); + } + return css; + }, function(e) { + d.reject(errorHandler(e)); + }).then(function(css) { + d.resolve(css); + }, function(err) { + d.reject(errorHandler(err)); + }); + } catch (e) { + d.reject(errorHandler(e)) } + return d.promise; + function getLine(index) { return index ? (input.slice(0, index).match(/\n/g) || "").length : null; } @@ -367,7 +372,7 @@ less.Parser = function Parser(env) { for (var n = i, column = -1; n >= 0 && input.charAt(n) !== '\n'; n--) { column++ } - error = { + throw { name: "ParseError", message: "Syntax Error on line " + line, index: i, @@ -382,11 +387,7 @@ less.Parser = function Parser(env) { }; } - if (this.imports.queue.length > 0) { - finish = function () { callback(error, root) }; - } else { - callback(error, root); - } + return root; }, // @@ -958,7 +959,7 @@ less.Parser = function Parser(env) { if ($(/^@import\s+/) && (path = $(this.entities.quoted) || $(this.entities.url)) && $(';')) { - return new(tree.Import)(path, imports); + return tree.newImport(path, imports); } }, diff --git a/lib/less/rhino.js b/lib/less/rhino.js index ab1c88682..aac1e3cde 100644 --- a/lib/less/rhino.js +++ b/lib/less/rhino.js @@ -4,13 +4,13 @@ function loadStyleSheet(sheet, callback, reload, remaining) { var sheetName = name.slice(0, name.lastIndexOf('/') + 1) + sheet.href; var input = readFile(sheetName); var parser = new less.Parser(); - parser.parse(input, function (e, root) { - if (e) { - print("Error: " + e); - quit(1); - } - callback(root, sheet, { local: false, lastModified: 0, remaining: remaining }); - }); + try { + var root = parser.parse(input); + } catch (e) { + print("Error: " + e); + quit(1); + } + callback(root, sheet, { local: false, lastModified: 0, remaining: remaining}); // callback({}, sheet, { local: true, remaining: remaining }); } @@ -42,11 +42,9 @@ function writeFile(filename, content) { var result; var parser = new less.Parser(); - parser.parse(input, function (e, root) { - if (e) { - quit(1); - } else { - result = root.toCSS(); + try { + var root = parser.parse(input); + root.toCSS().then(function(result) { if (output) { writeFile(output, result); print("Written to " + output); @@ -54,7 +52,12 @@ function writeFile(filename, content) { print(result); } quit(0); - } - }); + }, function(err) { + print("Error: " + err); + quit(1); + }); + } catch (e) { + quit(1); + } print("done"); }(arguments)); diff --git a/lib/less/tree/import.js b/lib/less/tree/import.js index 427c1095e..5e7deba8e 100644 --- a/lib/less/tree/import.js +++ b/lib/less/tree/import.js @@ -1,4 +1,4 @@ -(function (tree) { +(function (tree, when) { // // CSS @import node // @@ -11,31 +11,39 @@ // `import,push`, we also pass it a callback, which it'll call once // the file has been fetched, and parsed. // -tree.Import = function (path, imports) { - var that = this; - - this._path = path; - // The '.less' extension is optional - if (path instanceof tree.Quoted) { - this.path = /\.(le?|c)ss(\?.*)?$/.test(path.value) ? path.value : path.value + '.less'; +tree.newImport = function(path, imports) { + var css = /css(\?.*)?$/.test(path.value.value || path.value); + if (css) { + return new tree.CSSImport(path, imports); } else { - this.path = path.value.value || path.value; + return new tree.Import(path, imports); } +} - this.css = /css(\?.*)?$/.test(this.path); +tree.CSSImport = function (path) { + this._path = path; +}; - // Only pre-compile .less files - if (! this.css) { - imports.push(this.path, function (root) { - if (! root) { - throw new(Error)("Error parsing " + that.path); - } - that.root = root; - }); +tree.CSSImport.prototype = { + toCSS: function () { + return "@import " + this._path.toCSS() + ';\n'; + + }, + eval: function (env) { + if (this._path instanceof tree.Quoted) { + this._path = this._path.eval(env); + } + return this; } }; + +tree.Import = function (path, imports) { + this._path = path; + this._imports = imports; +}; + // // The actual import node doesn't return anything, when converted to CSS. // The reason is that it's used at the evaluation stage, so that the rules @@ -47,31 +55,28 @@ tree.Import = function (path, imports) { // tree.Import.prototype = { toCSS: function () { - if (this.css) { - return "@import " + this._path.toCSS() + ';\n'; - } else { - return ""; - } + return ""; }, eval: function (env) { - var ruleset; + var path = this._path; + var d = when.defer(); - if (this.css) { - return this; + // The '.less' extension is optional + if (path instanceof tree.Quoted) { + path = path.eval(env); + path = /\.(le?|c)ss(\?.*)?$/.test(path.value) ? path.value : path.value + '.less'; } else { - ruleset = new(tree.Ruleset)(null, this.root.rules.slice(0)); + path = path.value.value || path.value; + } - for (var i = 0; i < ruleset.rules.length; i++) { - if (ruleset.rules[i] instanceof tree.Import) { - Array.prototype - .splice - .apply(ruleset.rules, - [i, 1].concat(ruleset.rules[i].eval(env))); - } + this._imports.push(path, function (root) { + if (! root) { + throw new(Error)("Error parsing " + path); } - return ruleset.rules; - } + when.chain(root.evalImports(env, root.clone()), d); + }); + return d.promise; } }; -})(require('less/tree')); +})(require('less/tree'), require('less/when')); diff --git a/lib/less/tree/ruleset.js b/lib/less/tree/ruleset.js index cc9a60aec..1a96cb5f9 100644 --- a/lib/less/tree/ruleset.js +++ b/lib/less/tree/ruleset.js @@ -1,4 +1,4 @@ -(function (tree) { +(function (tree, when) { tree.Ruleset = function (selectors, rules) { this.selectors = selectors; @@ -7,77 +7,107 @@ tree.Ruleset = function (selectors, rules) { }; tree.Ruleset.prototype = { eval: function (env) { - var ruleset = new(tree.Ruleset)(this.selectors, this.rules.slice(0)); - - ruleset.root = this.root; + var ruleset = this.clone(); // push the current ruleset to the frames stack env.frames.unshift(ruleset); - // Evaluate imports - if (ruleset.root) { + var process = function(ruleset) { + // Store the frames around mixin definitions, + // so they can be evaluated like closures when the time comes. for (var i = 0; i < ruleset.rules.length; i++) { - if (ruleset.rules[i] instanceof tree.Import) { + if (ruleset.rules[i] instanceof tree.mixin.Definition) { + ruleset.rules[i].frames = env.frames.slice(0); + } + } + + // Evaluate mixin calls. + for (var i = 0; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.mixin.Call) { Array.prototype.splice .apply(ruleset.rules, [i, 1].concat(ruleset.rules[i].eval(env))); } } - } - // Store the frames around mixin definitions, - // so they can be evaluated like closures when the time comes. - for (var i = 0; i < ruleset.rules.length; i++) { - if (ruleset.rules[i] instanceof tree.mixin.Definition) { - ruleset.rules[i].frames = env.frames.slice(0); - } - } + // Evaluate everything else + for (var i = 0, rule; i < ruleset.rules.length; i++) { + rule = ruleset.rules[i]; - // Evaluate mixin calls. - for (var i = 0; i < ruleset.rules.length; i++) { - if (ruleset.rules[i] instanceof tree.mixin.Call) { - Array.prototype.splice - .apply(ruleset.rules, [i, 1].concat(ruleset.rules[i].eval(env))); + if (! (rule instanceof tree.mixin.Definition)) { + ruleset.rules[i] = rule.eval ? rule.eval(env) : rule; + } } - } - // Evaluate everything else - for (var i = 0, rule; i < ruleset.rules.length; i++) { - rule = ruleset.rules[i]; + // Pop the stack + env.frames.shift(); - if (! (rule instanceof tree.mixin.Definition)) { - ruleset.rules[i] = rule.eval ? rule.eval(env) : rule; - } + return ruleset; } - // Pop the stack - env.frames.shift(); + // Evaluate imports + if (this.root) { + var deferred = when.defer(); + this.evalImports(env, ruleset).then(function() { + return process(ruleset); + }, function(e) { + deferred.reject(e); + }).then(function(x) { + deferred.resolve(x); + }, function(e) { + deferred.reject(e); + }); + return deferred.promise; + } else { + return process(ruleset); + } + }, + clone: function() { + var ruleset = new(tree.Ruleset)(this.selectors, this.rules.slice(0)); + ruleset.root = this.root; return ruleset; }, + + evalImports: function(env, ruleset) { + var deferred = when.defer(); + var i = 0; + + var iterator = function(from) { + for (i = from; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.Import) { + ruleset.rules[i].eval(env).then(function(rules) { + [].splice.apply(ruleset.rules, [i, 1].concat(rules)); + var skip = (typeof rules.length == 'number') ? rules.length : 1; + iterator(i+skip); + }); + return; + } + } + deferred.resolve(ruleset.rules); + }; + iterator(0); + return deferred.promise; + }, + match: function (args) { return !args || args.length === 0; }, + variables: function () { - if (this._variables) { return this._variables } - else { - return this._variables = this.rules.reduce(function (hash, r) { - if (r instanceof tree.Rule && r.variable === true) { - hash[r.name] = r; - } - return hash; - }, {}); - } + return this._variables = this.rules.reduce(function (hash, r) { + if (r instanceof tree.Rule && r.variable === true) { + hash[r.name] = r; + } + return hash; + }, {}); }, variable: function (name) { return this.variables()[name]; }, rulesets: function () { - if (this._rulesets) { return this._rulesets } - else { - return this._rulesets = this.rules.filter(function (r) { - return (r instanceof tree.Ruleset) || (r instanceof tree.mixin.Definition); - }); - } + return this._rulesets = this.rules.filter(function (r) { + return (r instanceof tree.Ruleset) || (r instanceof tree.mixin.Definition); + }); }, find: function (selector, self) { self = self || this; @@ -145,7 +175,7 @@ tree.Ruleset.prototype = { rules.push(rule.value.toString()); } } - } + } rulesets = rulesets.join(''); @@ -209,4 +239,4 @@ tree.Ruleset.prototype = { } } }; -})(require('less/tree')); +})(require('less/tree'), require('less/when')); diff --git a/lib/less/when.js b/lib/less/when.js new file mode 100644 index 000000000..8a78a23a0 --- /dev/null +++ b/lib/less/when.js @@ -0,0 +1,361 @@ +/** + * @license Copyright (c) 2011 Brian Cavalier + * LICENSE: see the LICENSE.txt file. If file is missing, this file is subject + * to the MIT License at: http://www.opensource.org/licenses/mit-license.php. + */ + +// +// when.js 0.9.3 +// +(function(scope, undef) { + + // No-op function used in function replacement in various + // places below. + function noop() {} + + // Use freeze if it exists + var freeze = Object.freeze || noop; + + // Creates a new, CommonJS compliant, Deferred with fully isolated + // resolver and promise parts, either or both of which may be given out + // safely to consumers. + // The Deferred itself has the full API: resolve, reject, progress, and + // then. The resolver has resolve, reject, and progress. The promise + // only has then. + function defer() { + var deferred, promise, resolver, result, listeners, tail, + _then, _progress, complete; + + _then = function(callback, errback, progback) { + var d, listener; + + listener = { + deferred: (d = defer()), + resolve: callback, + reject: errback, + progress: progback + }; + + if(listeners) { + // Append new listener if linked list already initialized + tail = tail.next = listener; + } else { + // Init linked list + listeners = tail = listener; + } + + return d.promise; + }; + + function then(callback, errback, progback) { + return _then(callback, errback, progback); + } + + function resolve(val) { + complete('resolve', val); + } + + function reject(err) { + complete('reject', err); + } + + _progress = function(update) { + var listener, progress; + + listener = listeners; + + while(listener) { + progress = listener.progress; + if(progress) progress(update); + listener = listener.next; + } + }; + + function progress(update) { + _progress(update); + } + + complete = function(which, val) { + // Save original _then + var origThen = _then; + + // Replace _then with one that immediately notifies + // with the result. + _then = function newThen(callback, errback) { + var promise = origThen(callback, errback); + notify(which); + return promise; + }; + + // Replace complete so that this Deferred + // can only be completed once. Note that this leaves + // notify() intact so that it can be used in the + // rewritten _then above. + // Replace _progress, so that subsequent attempts + // to issue progress throw. + complete = _progress = function alreadyCompleted() { + throw new Error("already completed"); + }; + + // Final result of this Deferred. This is immutable + result = val; + + // Notify listeners + notify(which); + }; + + function notify(which) { + // Traverse all listeners registered directly with this Deferred, + // also making sure to handle chained thens + while(listeners) { + var listener, ldeferred, newResult, handler; + + listener = listeners; + ldeferred = listener.deferred; + listeners = listeners.next; + + handler = listener[which]; + if(handler) { + try { + newResult = handler(result); + + if(isPromise(newResult)) { + // If the handler returned a promise, chained deferreds + // should complete only after that promise does. + _chain(newResult, ldeferred); + + } else { + // Complete deferred from chained then() + // FIXME: Which is correct? + // The first always mutates the chained value, even if it is undefined + // The second will only mutate if newResult !== undefined + // ldeferred[which](newResult); + + ldeferred[which](newResult === undef ? result : newResult); + + } + } catch(e) { + // Exceptions cause chained deferreds to complete + // TODO: Should it *also* switch this promise's handlers to failed?? + // I think no. + // which = 'reject'; + + ldeferred.reject(e); + } + } + } + } + + // The full Deferred object, with both Promise and Resolver parts + deferred = {}; + + // Promise and Resolver parts + + // Expose Promise API + promise = deferred.promise = { + then: (deferred.then = then) + }; + + // Expose Resolver API + resolver = deferred.resolver = { + resolve: (deferred.resolve = resolve), + reject: (deferred.reject = reject), + progress: (deferred.progress = progress) + }; + + // Freeze Promise and Resolver APIs + freeze(promise); + freeze(resolver); + + return deferred; + } + + // Determines if promiseOrValue is a promise or not. Uses the feature + // test from http://wiki.commonjs.org/wiki/Promises/A to determine if + // promiseOrValue is a promise. + // + // Parameters: + // promiseOrValue - anything + // + // Return true if promiseOrValue is a promise. + function isPromise(promiseOrValue) { + return promiseOrValue && typeof promiseOrValue.then === 'function'; + } + + // Register a handler for a promise or immediate value + // + // Parameters: + // promiseOrValue - anything + // + // Returns a new promise that will resolve: + // 1. if promiseOrValue is a promise, when promiseOrValue resolves + // 2. if promiseOrValue is a value, immediately + function when(promiseOrValue, callback, errback, progressHandler) { + var deferred, resolve, reject; + + deferred = defer(); + + resolve = callback ? callback : function(val) { return val; }; + reject = errback ? errback : function(err) { return err; }; + + if(isPromise(promiseOrValue)) { + // If it's a promise, ensure that deferred will complete when promiseOrValue + // completes. + promiseOrValue.then(resolve, reject, + function(update) { progressHandler(update); } + ); + _chain(promiseOrValue, deferred); + + } else { + // If it's a value, resolve immediately + deferred.resolve(resolve(promiseOrValue)); + + } + + return deferred.promise; + } + + // Return a promise that will resolve when howMany of the supplied promisesOrValues + // have resolved. The resolution value of the returned promise will be an array of + // length howMany containing the resolutions values of the triggering promisesOrValues. + function some(promisesOrValues, howMany, callback, errback, progressHandler) { + var toResolve, results, ret, deferred, resolver, rejecter, handleProgress; + + toResolve = Math.max(0, Math.min(howMany, promisesOrValues.length)); + results = []; + deferred = defer(); + ret = (callback || errback || progressHandler) + ? deferred.then(callback, errback, progressHandler) + : deferred.promise; + + // Resolver for promises. Captures the value and resolves + // the returned promise when toResolve reaches zero. + // Overwrites resolver var with a noop once promise has + // be resolved to cover case where n < promises.length + resolver = function(val) { + results.push(val); + if(--toResolve === 0) { + resolver = handleProgress = noop; + deferred.resolve(results); + } + }; + + // Wrapper so that resolver can be replaced + function resolve(val) { + resolver(val); + } + + // Rejecter for promises. Rejects returned promise + // immediately, and overwrites rejecter var with a noop + // once promise to cover case where n < promises.length. + // TODO: Consider rejecting only when N (or promises.length - N?) + // promises have been rejected instead of only one? + rejecter = function(err) { + rejecter = handleProgress = noop; + deferred.reject(err); + }; + + // Wrapper so that rejecer can be replaced + function reject(err) { + rejecter(err); + } + + handleProgress = function(update) { + deferred.progress(update); + }; + + function progress(update) { + handleProgress(update); + } + + if(toResolve === 0) { + deferred.resolve(results); + } else { + var promiseOrValue, i = 0; + while((promiseOrValue = promisesOrValues[i++])) { + when(promiseOrValue, resolve, reject, progress); + } + } + + return ret; + } + + // Return a promise that will resolve only once all the supplied promisesOrValues + // have resolved. The resolution value of the returned promise will be an array + // containing the resolution values of each of the promisesOrValues. + function all(promisesOrValues, callback, errback, progressHandler) { + return some(promisesOrValues, promisesOrValues.length, callback, errback, progressHandler); + } + + // Return a promise that will resolve when any one of the supplied promisesOrValues + // has resolved. The resolution value of the returned promise will be the resolution + // value of the triggering promiseOrValue. + function any(promisesOrValues, callback, errback, progressHandler) { + return some(promisesOrValues, 1, callback, errback, progressHandler); + } + + // Ensure that resolution of promiseOrValue will complete resolver with the completion + // value of promiseOrValue, or instead with optionalValue if it is provided. + // + // Parameters: + // promiseOrValue - Promise, that when completed, will trigger completion of resolver, + // or value that will trigger immediate resolution of resolver + // resolver - Resolver to complete when promise completes + // resolveValue - optional value to use as the resolution value + // used to resolve second, rather than the resolution + // value of first. + // + // Returns a new promise that will complete when promiseOrValue is completed, + // with the completion value of promiseOrValue, or instead with optionalValue if it + // is provided. + function chain(promiseOrValue, resolver, resolveValue) { + var inputPromise, initChain; + + inputPromise = when(promiseOrValue); + + // Check against args length instead of resolvedValue === undefined, since + // undefined may be a valid resolution value. + initChain = arguments.length > 2 + ? function(resolver) { return _chain(inputPromise, resolver, resolveValue) } + : function(resolver) { return _chain(inputPromise, resolver); }; + + // Setup chain to supplied resolver + initChain(resolver); + + // Setup chain to new promise + return initChain(scope.defer()).promise; + } + + // Internal chain helper that does not create a new deferred/promise + // Always returns it's 2nd arg. + // NOTE: deferred must be a when.js deferred, or a resolver whose functions + // can be called without their original context. + function _chain(promise, deferred, resolveValue) { + promise.then( + // If resolveValue was supplied, need to wrap up a new function + // If not, can use deferred.resolve directly + arguments.length > 2 + ? function() { deferred.resolve(resolveValue) } + : deferred.resolve, + deferred.reject, + deferred.progress + ); + + return deferred; + } + + // + // Public API + // + + scope.defer = defer; + + scope.isPromise = isPromise; + scope.some = some; + scope.all = all; + scope.any = any; + scope.chain = chain; + scope.when = when; + + +})(require('less/when')); diff --git a/test/less-test.js b/test/less-test.js index 8dc162e75..6792bd028 100644 --- a/test/less-test.js +++ b/test/less-test.js @@ -42,21 +42,20 @@ function toCSS(path, callback) { fs.readFile(path, 'utf-8', function (e, str) { if (e) { return callback(e) } - new(less.Parser)({ - paths: [require('path').dirname(path)], - optimization: 0 - }).parse(str, function (err, tree) { - if (err) { + try { + var root = new(less.Parser)({ + paths: [require('path').dirname(path)], + optimization: 0 + }).parse(str); + + root.toCSS().then(function(css) { + callback(null, css); + }, function(err) { callback(err); - } else { - try { - css = tree.toCSS(); - callback(null, css); - } catch (e) { - callback(e); - } - } - }); + }); + } catch (err) { + callback(err); + } }); } diff --git a/test/less/import.less b/test/less/import.less index 42be3c1e5..ed7bb8b4f 100644 --- a/test/less/import.less +++ b/test/less/import.less @@ -1,5 +1,8 @@ -@import url("import/import-test-a.less"); //@import url("import/import-test-a.less"); +//@import url("import/import-test-a.less"); + +@base_path: "import/"; +@import "@{base_path}import-test-a.less"; @import url(http://fonts.googleapis.com/css?family=Open+Sans); #import-test { diff --git a/test/less/import/import-test-b.less b/test/less/import/import-test-b.less index ce2d35a83..cc2ff97ed 100644 --- a/test/less/import/import-test-b.less +++ b/test/less/import/import-test-b.less @@ -1,3 +1,5 @@ +@test_d: "test-d"; + @import "import-test-c"; @b: 100%; diff --git a/test/less/import/import-test-c.less b/test/less/import/import-test-c.less index 111266ba8..b28ed58c8 100644 --- a/test/less/import/import-test-c.less +++ b/test/less/import/import-test-c.less @@ -1,5 +1,5 @@ -@import "import-test-d.css"; +@import "import-@{test_d}.css"; @c: red; #import {