diff --git a/lib/less/environment/abstract-file-manager.js b/lib/less/environment/abstract-file-manager.js index 96b932838..037eac8e5 100644 --- a/lib/less/environment/abstract-file-manager.js +++ b/lib/less/environment/abstract-file-manager.js @@ -16,8 +16,12 @@ abstractFileManager.prototype.getPath = function (filename) { return filename.slice(0, j + 1); }; +abstractFileManager.prototype.tryAppendExtension = function(path, ext) { + return /(\.[a-z]*$)|([\?;].*)$/.test(path) ? path : path + ext; +}; + abstractFileManager.prototype.tryAppendLessExtension = function(path) { - return /(\.[a-z]*$)|([\?;].*)$/.test(path) ? path : path + '.less'; + return this.tryAppendExtension(path, '.less'); }; abstractFileManager.prototype.supportsSync = function() { diff --git a/lib/less/functions/function-caller.js b/lib/less/functions/function-caller.js index dcce35cfb..d5358b3f5 100644 --- a/lib/less/functions/function-caller.js +++ b/lib/less/functions/function-caller.js @@ -1,12 +1,12 @@ -var functionRegistry = require("./function-registry"), - Expression = require("../tree/expression"); +var Expression = require("../tree/expression"); var functionCaller = function(name, context, index, currentFileInfo) { this.name = name.toLowerCase(); - this.func = functionRegistry.get(this.name); this.index = index; this.context = context; this.currentFileInfo = currentFileInfo; + + this.func = context.frames[0].functionRegistry.get(this.name); }; functionCaller.prototype.isValid = function() { return Boolean(this.func); diff --git a/lib/less/functions/function-registry.js b/lib/less/functions/function-registry.js index d39230d1d..bc21bcc8e 100644 --- a/lib/less/functions/function-registry.js +++ b/lib/less/functions/function-registry.js @@ -1,18 +1,29 @@ -module.exports = { - _data: {}, - add: function(name, func) { - if (this._data.hasOwnProperty(name)) { - //TODO warn +function makeRegistry( base ) { + return { + _data: {}, + add: function(name, func) { + // precautionary case conversion, as later querying of + // the registry by function-caller uses lower case as well. + name = name.toLowerCase(); + + if (this._data.hasOwnProperty(name)) { + //TODO warn + } + this._data[name] = func; + }, + addMultiple: function(functions) { + Object.keys(functions).forEach( + function(name) { + this.add(name, functions[name]); + }.bind(this)); + }, + get: function(name) { + return this._data[name] || ( base && base.get( name )); + }, + inherit : function() { + return makeRegistry( this ); } - this._data[name] = func; - }, - addMultiple: function(functions) { - Object.keys(functions).forEach( - function(name) { - this.add(name, functions[name]); - }.bind(this)); - }, - get: function(name) { - return this._data[name]; - } -}; + }; +} + +module.exports = makeRegistry( null ); \ No newline at end of file diff --git a/lib/less/import-manager.js b/lib/less/import-manager.js index 22784d77f..79d2f3fec 100644 --- a/lib/less/import-manager.js +++ b/lib/less/import-manager.js @@ -1,5 +1,6 @@ var contexts = require("./contexts"), - Parser = require('./parser/parser'); + Parser = require('./parser/parser'), + FunctionImporter = require('./plugins/function-importer'); module.exports = function(environment) { @@ -65,7 +66,7 @@ module.exports = function(environment) { } if (tryAppendLessExtension) { - path = fileManager.tryAppendLessExtension(path); + path = fileManager.tryAppendExtension(path, importOptions.plugin ? ".js" : ".less"); } var loadFileCallback = function(loadedFile) { @@ -101,7 +102,11 @@ module.exports = function(environment) { newFileInfo.reference = true; } - if (importOptions.inline) { + if (importOptions.plugin) { + new FunctionImporter(newEnv, newFileInfo).eval(contents, function (e, root) { + fileParsedFunc(e, root, resolvedFilename); + }); + } else if (importOptions.inline) { fileParsedFunc(null, contents, resolvedFilename); } else { new Parser(newEnv, importManager, newFileInfo).parse(contents, function (e, root) { diff --git a/lib/less/parser/parser.js b/lib/less/parser/parser.js index df309789b..f6d45eda4 100644 --- a/lib/less/parser/parser.js +++ b/lib/less/parser/parser.js @@ -1290,6 +1290,41 @@ var Parser = function Parser(context, imports, fileInfo) { } }, + // + // A @plugin directive, used to import compiler extensions dynamically. + // + // @plugin "lib"; + // + // Depending on our environment, importing is done differently: + // In the browser, it's an XHR request, in Node, it would be a + // file-system operation. The function used for importing is + // stored in `import`, which we pass to the Import constructor. + // + plugin: function () { + var path, + index = parserInput.i, + dir = parserInput.$re(/^@plugin?\s+/); + + if (dir) { + var options = { plugin : true }; + + if ((path = this.entities.quoted() || this.entities.url())) { + + if (!parserInput.$(';')) { + parserInput.i = index; + error("missing semi-colon on plugin"); + } + + return new(tree.Import)(path, null, options, index, fileInfo); + } + else + { + parserInput.i = index; + error("malformed plugin statement"); + } + } + }, + // // A CSS Directive // @@ -1301,7 +1336,7 @@ var Parser = function Parser(context, imports, fileInfo) { if (parserInput.currentChar() !== '@') { return; } - value = this['import']() || this.media(); + value = this['import']() || this.plugin() || this.media(); if (value) { return value; } diff --git a/lib/less/plugins/function-importer.js b/lib/less/plugins/function-importer.js new file mode 100644 index 000000000..b863998b2 --- /dev/null +++ b/lib/less/plugins/function-importer.js @@ -0,0 +1,35 @@ +var LessError = require('../less-error'), + tree = require("../tree"); + +var FunctionImporter = module.exports = function FunctionImporter(context, fileInfo) { + this.fileInfo = fileInfo; +}; + +FunctionImporter.prototype.eval = function(contents, callback) { + var loaded = {}, + loader, + registry; + + registry = { + add: function(name, func) { + loaded[name] = func; + }, + addMultiple: function(functions) { + Object.keys(functions).forEach(function(name) { + loaded[name] = functions[name]; + }); + } + }; + + try { + loader = new Function("functions", "tree", "fileInfo", contents); + loader(registry, tree, this.fileInfo); + } catch(e) { + callback(new LessError({ + message: "Plugin evaluation error: '" + e.name + ': ' + e.message.replace(/["]/g, "'") + "'" , + filename: this.fileInfo.filename + }), null ); + } + + callback(null, { functions: loaded }); +}; diff --git a/lib/less/tree/import.js b/lib/less/tree/import.js index e88e6dcef..0d3c68079 100644 --- a/lib/less/tree/import.js +++ b/lib/less/tree/import.js @@ -50,7 +50,7 @@ Import.prototype.accept = function (visitor) { this.features = visitor.visit(this.features); } this.path = visitor.visit(this.path); - if (!this.options.inline && this.root) { + if (!this.options.plugin && !this.options.inline && this.root) { this.root = visitor.visit(this.root); } }; @@ -109,7 +109,8 @@ Import.prototype.evalPath = function (context) { return path; }; Import.prototype.eval = function (context) { - var ruleset, features = this.features && this.features.eval(context); + var ruleset, registry, + features = this.features && this.features.eval(context); if (this.skip) { if (typeof this.skip === "function") { @@ -120,7 +121,13 @@ Import.prototype.eval = function (context) { } } - if (this.options.inline) { + if (this.options.plugin) { + registry = context.frames[0] && context.frames[0].functionRegistry; + if ( registry && this.root.functions ) { + registry.addMultiple( this.root.functions ); + } + return []; + } else if (this.options.inline) { var contents = new Anonymous(this.root, 0, {filename: this.importedFilename}, true, true); return this.features ? new Media([contents], this.features.value) : [contents]; } else if (this.css) { diff --git a/lib/less/tree/mixin-definition.js b/lib/less/tree/mixin-definition.js index b81dd4eb8..9a92ed01b 100644 --- a/lib/less/tree/mixin-definition.js +++ b/lib/less/tree/mixin-definition.js @@ -44,6 +44,7 @@ Definition.prototype.evalParams = function (context, mixinEnv, args, evaldArgume i, j, val, name, isNamedFound, argIndex, argsLength = 0; mixinEnv = new contexts.Eval(mixinEnv, [frame].concat(mixinEnv.frames)); + frame.functionRegistry = context.frames[0].functionRegistry.inherit(); if (args) { args = args.slice(0); diff --git a/lib/less/tree/ruleset.js b/lib/less/tree/ruleset.js index 9e55c5bf0..ea2948f5b 100644 --- a/lib/less/tree/ruleset.js +++ b/lib/less/tree/ruleset.js @@ -4,6 +4,7 @@ var Node = require("./node"), Element = require("./element"), Paren = require("./paren"), contexts = require("../contexts"), + globalFunctionRegistry = require("../functions/function-registry"), defaultFunc = require("../functions/default"), getDebugInfo = require("./debug-info"); @@ -66,6 +67,10 @@ Ruleset.prototype.eval = function (context) { rules.length = 0; } + // inherit a function registry from the frames stack when possible; + // otherwise from the global registry + ruleset.functionRegistry = ((context.frames[0] && context.frames[0].functionRegistry) || globalFunctionRegistry).inherit(); + // push the current ruleset to the frames stack var ctxFrames = context.frames; ctxFrames.unshift(ruleset); diff --git a/test/css/import-plugin-scoped.css b/test/css/import-plugin-scoped.css new file mode 100644 index 000000000..e42ec6478 --- /dev/null +++ b/test/css/import-plugin-scoped.css @@ -0,0 +1,6 @@ +.in-scope { + result: PASS; +} +.out-of-scope { + result: test(); +} diff --git a/test/css/import-plugin-tiered.css b/test/css/import-plugin-tiered.css new file mode 100644 index 000000000..7960b9f3f --- /dev/null +++ b/test/css/import-plugin-tiered.css @@ -0,0 +1,6 @@ +.test { + result: PASS; +} +.test-again { + result: PASS; +} diff --git a/test/css/import-plugin.css b/test/css/import-plugin.css new file mode 100644 index 000000000..1c337756a --- /dev/null +++ b/test/css/import-plugin.css @@ -0,0 +1,3 @@ +.test { + result: PASS; +} diff --git a/test/less/import-plugin-scoped.less b/test/less/import-plugin-scoped.less new file mode 100644 index 000000000..7c0fa7825 --- /dev/null +++ b/test/less/import-plugin-scoped.less @@ -0,0 +1,9 @@ +.in-scope { + @plugin "./plugins/test"; + result : test(); +} + +.out-of-scope { + result : test(); +} + diff --git a/test/less/import-plugin-tiered.less b/test/less/import-plugin-tiered.less new file mode 100644 index 000000000..eb487738b --- /dev/null +++ b/test/less/import-plugin-tiered.less @@ -0,0 +1,5 @@ +@import "import-plugin"; + +.test-again { + result : test(); +} \ No newline at end of file diff --git a/test/less/import-plugin.less b/test/less/import-plugin.less new file mode 100644 index 000000000..7ee43e30a --- /dev/null +++ b/test/less/import-plugin.less @@ -0,0 +1,5 @@ +@plugin "./plugins/test"; + +.test { + result : test(); +} \ No newline at end of file diff --git a/test/less/plugins/test.js b/test/less/plugins/test.js new file mode 100644 index 000000000..f54ee82b1 --- /dev/null +++ b/test/less/plugins/test.js @@ -0,0 +1,4 @@ + +functions.add("test", function() { + return new tree.Anonymous( "PASS" ); +});