Merge pull request #2479 from rjgotten/import-plugin

Import plugin
This commit is contained in:
Luke Page
2015-03-15 17:44:47 +00:00
16 changed files with 169 additions and 28 deletions

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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 );

View File

@@ -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) {

View File

@@ -1304,6 +1304,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
//
@@ -1315,7 +1350,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;
}

View File

@@ -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 });
};

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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);

View File

@@ -0,0 +1,6 @@
.in-scope {
result: PASS;
}
.out-of-scope {
result: test();
}

View File

@@ -0,0 +1,6 @@
.test {
result: PASS;
}
.test-again {
result: PASS;
}

View File

@@ -0,0 +1,3 @@
.test {
result: PASS;
}

View File

@@ -0,0 +1,9 @@
.in-scope {
@plugin "./plugins/test";
result : test();
}
.out-of-scope {
result : test();
}

View File

@@ -0,0 +1,5 @@
@import "import-plugin";
.test-again {
result : test();
}

View File

@@ -0,0 +1,5 @@
@plugin "./plugins/test";
.test {
result : test();
}

View File

@@ -0,0 +1,4 @@
functions.add("test", function() {
return new tree.Anonymous( "PASS" );
});