Simplified API on less object

e.g. "less.Ruleset()" instead of "new less.tree.Ruleset()"

Auto-casting of string values into nodes for AtRule, Declaration, Selector, Value
e.g. "less.Selector('&.a')" instead of "new tree.Selector(new tree.Element(new tree.Combinator('&'), '.a'))"
This commit is contained in:
Matthew Dean
2016-07-19 01:30:34 -07:00
parent a38f8a1eb7
commit 39150d513a
15 changed files with 192 additions and 93 deletions

View File

@@ -54,8 +54,8 @@ AbstractPluginLoader.prototype.evalPlugin = function(contents, context, pluginOp
registry = functionRegistry.create();
try {
loader = new Function("module", "require", "functions", "tree", "fileInfo", contents);
pluginObj = loader(localModule, this.require, registry, this.less.tree, fileInfo);
loader = new Function("module", "require", "functions", "tree", "less", "fileInfo", contents);
pluginObj = loader(localModule, this.require, registry, this.less.tree, this.less, fileInfo);
if (!pluginObj) {
pluginObj = localModule.exports;

View File

@@ -1,7 +1,7 @@
module.exports = function(environment, fileManagers) {
var SourceMapOutput, SourceMapBuilder, ParseTree, ImportManager, Environment;
var less = {
var initial = {
version: [3, 0, 0],
data: require('./data'),
tree: require('./tree'),
@@ -26,5 +26,28 @@ module.exports = function(environment, fileManagers) {
logger: require('./logger')
};
return less;
// Create a public API
var ctor = function(t) {
return function() {
var obj = Object.create(t.prototype);
t.apply(obj, Array.prototype.slice.call(arguments, 0));
return obj;
};
};
var t, api = Object.create(initial);
for (var n in initial.tree) {
t = initial.tree[n];
if (typeof t === "function") {
api[n] = ctor(t);
}
else {
api[n] = Object.create(null);
for (var o in t) {
api[n][o] = ctor(t[o]);
}
}
}
return api;
};

View File

@@ -80,6 +80,53 @@ var Parser = function Parser(context, imports, fileInfo) {
};
}
/**
* Used after initial parsing to create nodes on the fly
*
* @param {String} str - string to parse
* @param {Array} parseList - array of parsers to run input through e.g. ["value", "important"]
* @param {Number} currentIndex - start number to begin indexing
* @param {Object} fileInfo - fileInfo to attach to created nodes
*/
function parseNode(str, parseList, currentIndex, fileInfo, callback) {
var result, returnNodes = [];
var parser = parserInput;
try {
parser.start(str, false, function fail(msg, index) {
callback({
message: msg,
index: index + currentIndex
});
});
for(var x = 0, p, i; (p = parseList[x]); x++) {
i = parser.i;
result = parsers[p]();
if (result) {
result._index = i + currentIndex;
result._fileInfo = fileInfo;
returnNodes.push(result);
}
else {
returnNodes.push(null);
}
}
var endInfo = parser.end();
if (endInfo.isFinished) {
callback(null, returnNodes);
}
else {
callback(true, null);
}
} catch (e) {
throw new LessError({
index: e.index + currentIndex,
message: e.message
}, imports, fileInfo.filename);
}
}
//
// The Parser
//
@@ -87,6 +134,7 @@ var Parser = function Parser(context, imports, fileInfo) {
parserInput: parserInput,
imports: imports,
fileInfo: fileInfo,
parseNode: parseNode,
//
// Parse an input string into an abstract syntax tree,
// @param str A string containing 'less' markup
@@ -133,8 +181,8 @@ var Parser = function Parser(context, imports, fileInfo) {
});
tree.Node.prototype.parse = this;
root = new(tree.Ruleset)(null, this.parsers.primary());
root = new tree.Ruleset(null, this.parsers.primary());
tree.Node.prototype.rootNode = root;
root.root = true;
root.firstRoot = true;
@@ -1008,7 +1056,7 @@ var Parser = function Parser(context, imports, fileInfo) {
if (! e) {
parserInput.save();
if (parserInput.$char('(')) {
if ((v = this.selector()) && parserInput.$char(')')) {
if ((v = this.selector(false)) && parserInput.$char(')')) {
e = new(tree.Paren)(v);
parserInput.forget();
} else {
@@ -1059,14 +1107,8 @@ var Parser = function Parser(context, imports, fileInfo) {
}
},
//
// A CSS selector (see selector below)
// with less extensions e.g. the ability to extend and guard
//
lessSelector: function () {
return this.selector(true);
},
//
// A CSS Selector
// with less extensions e.g. the ability to extend and guard
//
// .class > div + h1
// li a:hover
@@ -1075,7 +1117,7 @@ var Parser = function Parser(context, imports, fileInfo) {
//
selector: function (isLess) {
var index = parserInput.i, elements, extendList, c, e, allExtends, when, condition;
isLess = isLess !== false;
while ((isLess && (extendList = this.extend())) || (isLess && (when = parserInput.$str("when"))) || (e = this.element())) {
if (when) {
condition = expect(this.conditions, 'expected condition');
@@ -1165,7 +1207,7 @@ var Parser = function Parser(context, imports, fileInfo) {
}
while (true) {
s = this.lessSelector();
s = this.selector();
if (!s) {
break;
}

View File

@@ -1,3 +1,4 @@
var utils = require('./utils');
/**
* Plugin Manager
*/
@@ -53,13 +54,6 @@ PluginManager.prototype.get = function(filename) {
return this.pluginCache[filename];
};
// Object.getPrototypeOf shim for visitor upgrade
if (!Object.getPrototypeOf) {
Object.getPrototypeOf = function getPrototypeOf(object) {
return object.constructor ? object.constructor.prototype : void 0;
};
}
function upgradeVisitors(visitor, oldType, newType) {
if (visitor['visit' + oldType] && !visitor['visit' + newType]) {
@@ -78,7 +72,7 @@ PluginManager.prototype.addVisitor = function(visitor) {
var proto;
// 2.x to 3.x visitor compatibility
try {
proto = Object.getPrototypeOf(visitor);
proto = utils.getPrototype(visitor);
upgradeVisitors(proto, 'Directive', 'AtRule');
upgradeVisitors(proto, 'Rule', 'Declaration');
}

View File

@@ -1,12 +1,14 @@
var Node = require("./node"),
Selector = require("./selector"),
Ruleset = require("./ruleset");
Ruleset = require("./ruleset"),
Value = require('./value'),
Anonymous = require('./anonymous');
var AtRule = function (name, value, rules, index, currentFileInfo, debugInfo, isRooted, visibilityInfo) {
var i;
this.name = name;
this.value = value;
this.value = (value instanceof Node) ? value : (value ? new Anonymous(value) : value);
if (rules) {
if (Array.isArray(rules)) {
this.rules = rules;

View File

@@ -1,10 +1,11 @@
var Node = require("./node"),
Value = require("./value"),
Keyword = require("./keyword");
Keyword = require("./keyword"),
Anonymous = require("./anonymous");
var Declaration = function (name, value, important, merge, index, currentFileInfo, inline, variable) {
this.name = name;
this.value = (value instanceof Node) ? value : new Value([value]); //value instanceof tree.Value || value instanceof tree.Ruleset ??
this.value = (value instanceof Node) ? value : new Value([value ? new Anonymous(value) : null]);
this.important = important ? ' ' + important.trim() : '';
this.merge = merge;
this._index = index;

View File

@@ -1,4 +1,4 @@
var tree = {};
var tree = Object.create(null);
tree.Node = require('./node');
tree.Alpha = require('./alpha');

View File

@@ -298,26 +298,21 @@ Ruleset.prototype.parseValue = function(toParse) {
var self = this;
function transformDeclaration(decl) {
if (decl.value instanceof Anonymous && !decl.parsed) {
try {
this.parse.parserInput.start(decl.value.value, false, function fail(msg, index) {
decl.parsed = true;
return decl;
this.parse.parseNode(
decl.value.value,
["value", "important"],
decl.value.getIndex(),
decl.fileInfo(),
function(err, result) {
if (err) {
decl.parsed = true;
}
if (result) {
decl.value = result[0];
decl.important = result[1] || '';
decl.parsed = true;
}
});
var result = this.parse.parsers.value();
var important = this.parse.parsers.important();
var endInfo = this.parse.parserInput.end();
if (endInfo.isFinished) {
decl.value = result;
decl.imporant = important;
decl.parsed = true;
}
} catch (e) {
throw new LessError({
index: e.index + decl.value.getIndex(),
message: e.message
}, this.parse.imports, decl.fileInfo().filename);
}
return decl;
}

View File

@@ -1,12 +1,13 @@
var Node = require("./node"),
Element = require("./element");
Element = require("./element"),
LessError = require("../less-error");
var Selector = function (elements, extendList, condition, index, currentFileInfo, visibilityInfo) {
this.elements = elements;
this.extendList = extendList;
this.condition = condition;
this._index = index;
this._fileInfo = currentFileInfo;
this.elements = this.getElements(elements);
if (!condition) {
this.evaldCondition = true;
}
@@ -27,6 +28,7 @@ Selector.prototype.accept = function (visitor) {
}
};
Selector.prototype.createDerived = function(elements, extendList, evaldCondition) {
elements = this.getElements(elements);
var info = this.visibilityInfo();
evaldCondition = (evaldCondition != null) ? evaldCondition : this.evaldCondition;
var newSelector = new Selector(elements, extendList || this.extendList, null, this.getIndex(), this.fileInfo(), info);
@@ -34,6 +36,25 @@ Selector.prototype.createDerived = function(elements, extendList, evaldCondition
newSelector.mediaEmpty = this.mediaEmpty;
return newSelector;
};
Selector.prototype.getElements = function(els) {
if (typeof els === "string") {
this.parse.parseNode(
els,
["selector"],
this._index,
this._fileInfo,
function(err, result) {
if (err) {
throw new LessError({
index: e.index + currentIndex,
message: e.message
}, this.parse.imports, this._fileInfo.filename);
}
els = result[0].elements;
});
}
return els;
};
Selector.prototype.createEmptySelectors = function() {
var el = new Element('', '&', this._index, this._fileInfo),
sels = [new Selector([el], null, null, this._index, this._fileInfo)];
@@ -45,7 +66,7 @@ Selector.prototype.match = function (other) {
len = elements.length,
olen, i;
other.CacheElements();
other.cacheElements();
olen = other._elements.length;
if (olen === 0 || len < olen) {
@@ -60,7 +81,7 @@ Selector.prototype.match = function (other) {
return olen; // return number of matched elements
};
Selector.prototype.CacheElements = function() {
Selector.prototype.cacheElements = function() {
if (this._elements) {
return;
}

View File

@@ -1,10 +1,17 @@
var Node = require("./node");
var Node = require("./node"),
Anonymous = require("./anonymous");
var Value = function (value) {
this.value = value;
if (!value) {
throw new Error("Value requires an array argument");
}
if (!Array.isArray(value)) {
this.value = [ value ];
}
else {
this.value = value;
}
};
Value.prototype = new Node();
Value.prototype.type = "Value";

View File

@@ -25,5 +25,18 @@ module.exports = {
copy[i] = arr[i];
}
return copy;
},
getPrototype: function(obj) {
if (Object.getPrototypeOf) {
return Object.getPrototypeOf(obj);
}
else {
if ("".__proto__ === String.prototype) {
return obj.__proto__;
}
else if (obj.constructor) {
return obj.constructor.prototype;
}
}
}
};

View File

@@ -10,21 +10,20 @@ function _noop(node) {
function indexNodeTypes(parent, ticker) {
// add .typeIndex to tree node types for lookup table
var key, child;
for (key in parent) {
if (parent.hasOwnProperty(key)) {
child = parent[key];
switch (typeof child) {
case "function":
// ignore bound functions directly on tree which do not have a prototype
// or aren't nodes
if (child.prototype && child.prototype.type) {
child.prototype.typeIndex = ticker++;
}
break;
case "object":
ticker = indexNodeTypes(child, ticker);
break;
}
for (key in parent) {
child = parent[key];
switch (typeof child) {
case "function":
// ignore bound functions directly on tree which do not have a prototype
// or aren't nodes
if (child.prototype && child.prototype.type) {
child.prototype.typeIndex = ticker++;
}
break;
case "object":
ticker = indexNodeTypes(child, ticker);
break;
}
}
return ticker;

View File

@@ -92,7 +92,7 @@
val2: foo;
}
test-directive("@charset"; '"utf-8"');
test-directive("@arbitrary"; "value after ()");
test-atrule("@charset"; '"utf-8"');
test-atrule("@arbitrary"; "value after ()");

View File

@@ -1,10 +1,10 @@
functions.addMultiple({
"test-comment": function() {
return new tree.Combinator(' ');
return less.Combinator(' ');
},
"test-directive": function(arg1, arg2) {
return new tree.Directive(arg1.value, new tree.Anonymous(arg2.value));
"test-atrule": function(arg1, arg2) {
return less.AtRule(arg1.value, arg2.value);
},
"test-extend": function() {
//TODO
@@ -22,7 +22,7 @@ functions.addMultiple({
//TODO
},
"test-ruleset-call": function() {
return new tree.Combinator(' ');
return less.Combinator(' ');
},
// Functions must return something, even if it's false/true
"test-undefined": function() {
@@ -33,52 +33,53 @@ functions.addMultiple({
},
// These cause root errors
"test-alpha": function() {
return new tree.Alpha(30);
return less.Alpha(30);
},
"test-assignment": function() {
return new tree.Assignment("bird", "robin");
return less.Assignment("bird", "robin");
},
"test-attribute": function() {
return new tree.Attribute("foo", "=", "bar");
return less.Attribute("foo", "=", "bar");
},
"test-call": function() {
return new tree.Call("foo");
return less.Call("foo");
},
"test-color": function() {
return new tree.Color([50, 50, 50]);
return less.Color([50, 50, 50]);
},
"test-condition": function() {
return new tree.Condition('<', new tree.Value([0]), new tree.Value([1]));
return less.Condition('<', less.Value([0]), less.Value([1]));
},
"test-detached-ruleset" : function() {
var decl = new tree.Declaration('prop', new tree.Anonymous('value'));
return new tree.DetachedRuleset(new tree.Ruleset("", [ decl ]));
var decl = less.Declaration('prop', 'value');
return less.DetachedRuleset(less.Ruleset("", [ decl ]));
},
"test-dimension": function() {
return new tree.Dimension(1, 'px');
return less.Dimension(1, 'px');
},
"test-element": function() {
return new tree.Element('+', 'a');
return less.Element('+', 'a');
},
"test-expression": function() {
return new tree.Expression([1, 2, 3]);
return less.Expression([1, 2, 3]);
},
"test-keyword": function() {
return new tree.Keyword('foo');
return less.Keyword('foo');
},
"test-operation": function() {
return new tree.Operation('+', [1, 2]);
return less.Operation('+', [1, 2]);
},
"test-quoted": function() {
return new tree.Quoted('"', 'foo');
return less.Quoted('"', 'foo');
},
"test-selector": function() {
return new tree.Selector([new tree.Element('a')]);
var sel = less.Selector('.a.b');
return sel;
},
"test-url": function() {
return new tree.URL('http://google.com');
return less.URL('http://google.com');
},
"test-value": function() {
return new tree.Value([1]);
return less.Value([1]);
}
});

View File

@@ -2,7 +2,8 @@
.block_1 {
color: red;
background-color: $color;
width: 50px;
@width: 50px;
width: @width;
height: ($width / 2);
@color: red;
border: 1px solid lighten($color, 10%);