Add modified (self altering) visitor pattern and class

This commit is contained in:
Luke Page
2013-02-21 17:59:28 +00:00
parent 9b256f2340
commit c56db94b7d
31 changed files with 338 additions and 177 deletions

View File

@@ -39,6 +39,7 @@ less:
${SRC}/tree/*.js\
${SRC}/tree.js\
${SRC}/env.js\
${SRC}/visitor.js\
${SRC}/browser.js\
build/amd.js >> ${DIST}
@@echo "})(window);" >> ${DIST}
@@ -60,6 +61,7 @@ rhino:
build/ecma-5.js\
${SRC}/parser.js\
${SRC}/env.js\
${SRC}/visitor.js\
${SRC}/functions.js\
${SRC}/colors.js\
${SRC}/tree/*.js\

View File

@@ -213,5 +213,6 @@ less.Parser.importer = function (file, paths, callback, env) {
require('./env');
require('./functions');
require('./colors');
require('./visitor.js');
for (var k in less) { exports[k] = less[k] }
for (var k in less) { exports[k] = less[k]; }

View File

@@ -4,13 +4,17 @@ tree.Alpha = function (val) {
this.value = val;
};
tree.Alpha.prototype = {
toCSS: function () {
return "alpha(opacity=" +
(this.value.toCSS ? this.value.toCSS() : this.value) + ")";
type: "Alpha",
accept: function (visitor) {
this.value = visitor.visit(this.value);
},
eval: function (env) {
if (this.value.eval) { this.value = this.value.eval(env) }
return this;
},
toCSS: function () {
return "alpha(opacity=" +
(this.value.toCSS ? this.value.toCSS() : this.value) + ")";
}
};

View File

@@ -4,6 +4,7 @@ tree.Anonymous = function (string) {
this.value = string.value || string;
};
tree.Anonymous.prototype = {
type: "Anonymous",
toCSS: function () {
return this.value;
},

View File

@@ -5,6 +5,10 @@ tree.Assignment = function (key, val) {
this.value = val;
};
tree.Assignment.prototype = {
type: "Assignment",
accept: function (visitor) {
this.value = visitor.visit(this.value);
},
toCSS: function () {
return this.key + '=' + (this.value.toCSS ? this.value.toCSS() : this.value);
},

View File

@@ -12,6 +12,10 @@ tree.Call = function (name, args, index, filename, rootpath, currentDirectory) {
this.currentDirectory = currentDirectory;
};
tree.Call.prototype = {
type: "Call",
accept: function (visitor) {
this.args = visitor.visit(this.args);
},
//
// When evaluating a function call,
// we either find the function in `tree.functions` [1],

View File

@@ -23,6 +23,7 @@ tree.Color = function (rgb, a) {
this.alpha = typeof(a) === 'number' ? a : 1;
};
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); },

View File

@@ -5,6 +5,7 @@ tree.Comment = function (value, silent) {
this.silent = !!silent;
};
tree.Comment.prototype = {
type: "Comment",
toCSS: function (env) {
return env.compress ? '' : this.value;
},

View File

@@ -7,36 +7,43 @@ tree.Condition = function (op, l, r, i, negate) {
this.index = i;
this.negate = negate;
};
tree.Condition.prototype.eval = function (env) {
var a = this.lvalue.eval(env),
b = this.rvalue.eval(env);
tree.Condition.prototype = {
type: "Condition",
accept: function (visitor) {
this.lvalue = visitor.visit(this.lvalue);
this.rvalue = visitor.visit(this.rvalue);
},
eval: function (env) {
var a = this.lvalue.eval(env),
b = this.rvalue.eval(env);
var i = this.index, result;
var i = this.index, result;
var result = (function (op) {
switch (op) {
case 'and':
return a && b;
case 'or':
return a || b;
default:
if (a.compare) {
result = a.compare(b);
} else if (b.compare) {
result = b.compare(a);
} else {
throw { type: "Type",
message: "Unable to perform comparison",
index: i };
}
switch (result) {
case -1: return op === '<' || op === '=<';
case 0: return op === '=' || op === '>=' || op === '=<';
case 1: return op === '>' || op === '>=';
}
}
})(this.op);
return this.negate ? !result : result;
var result = (function (op) {
switch (op) {
case 'and':
return a && b;
case 'or':
return a || b;
default:
if (a.compare) {
result = a.compare(b);
} else if (b.compare) {
result = b.compare(a);
} else {
throw { type: "Type",
message: "Unable to perform comparison",
index: i };
}
switch (result) {
case -1: return op === '<' || op === '=<';
case 0: return op === '=' || op === '>=' || op === '=<';
case 1: return op === '>' || op === '>=';
}
}
})(this.op);
return this.negate ? !result : result;
}
};
})(require('../tree'));

View File

@@ -10,6 +10,10 @@ tree.Dimension = function (value, unit) {
};
tree.Dimension.prototype = {
type: "Dimension",
accept: function (visitor) {
this.unit = visitor.visit(this.unit);
},
eval: function (env) {
return this;
},
@@ -172,6 +176,7 @@ tree.Unit = function (numerator, denominator) {
};
tree.Unit.prototype = {
type: "Unit",
clone: function () {
return new tree.Unit(this.numerator.slice(0), this.denominator.slice(0));
},

View File

@@ -11,6 +11,11 @@ tree.Directive = function (name, value) {
}
};
tree.Directive.prototype = {
type: "Directive",
accept: function (visitor) {
this.ruleset = visitor.visit(this.ruleset);
this.value = visitor.visit(this.value);
},
toCSS: function (ctx, env) {
if (this.ruleset) {
this.ruleset.root = true;

View File

@@ -13,17 +13,24 @@ tree.Element = function (combinator, value, index) {
}
this.index = index;
};
tree.Element.prototype.eval = function (env) {
return new(tree.Element)(this.combinator,
this.value.eval ? this.value.eval(env) : this.value,
this.index);
};
tree.Element.prototype.toCSS = function (env) {
var value = (this.value.toCSS ? this.value.toCSS(env) : this.value);
if (value == '' && this.combinator.value.charAt(0) == '&') {
return '';
} else {
return this.combinator.toCSS(env || {}) + value;
tree.Element.prototype = {
type: "Element",
accept: function (visitor) {
this.combinator = visitor.visit(this.combinator);
this.value = visitor.visit(this.value);
},
eval: function (env) {
return new(tree.Element)(this.combinator,
this.value.eval ? this.value.eval(env) : this.value,
this.index);
},
toCSS: function (env) {
var value = (this.value.toCSS ? this.value.toCSS(env) : this.value);
if (value == '' && this.combinator.value.charAt(0) == '&') {
return '';
} else {
return this.combinator.toCSS(env || {}) + value;
}
}
};
@@ -34,16 +41,19 @@ tree.Combinator = function (value) {
this.value = value ? value.trim() : "";
}
};
tree.Combinator.prototype.toCSS = function (env) {
return {
'' : '',
' ' : ' ',
':' : ' :',
'+' : env.compress ? '+' : ' + ',
'~' : env.compress ? '~' : ' ~ ',
'>' : env.compress ? '>' : ' > ',
'|' : env.compress ? '|' : ' | '
}[this.value];
tree.Combinator.prototype = {
type: "Combinator",
toCSS: function (env) {
return {
'' : '',
' ' : ' ',
':' : ' :',
'+' : env.compress ? '+' : ' + ',
'~' : env.compress ? '~' : ' ~ ',
'>' : env.compress ? '>' : ' > ',
'|' : env.compress ? '|' : ' | '
}[this.value];
}
};
})(require('../tree'));

View File

@@ -2,6 +2,10 @@
tree.Expression = function (value) { this.value = value; };
tree.Expression.prototype = {
type: "Expression",
accept: function (visitor) {
this.value = visitor.visit(this.value);
},
eval: function (env) {
var returnValue,
inParenthesis = this.parens && !this.parensInOp,

View File

@@ -6,34 +6,40 @@ tree.Extend = function Extend(elements, option, index) {
this.index = index;
};
tree.Extend.prototype.eval = function Extend_eval(env, selectors) {
var selfSelectors = findSelfSelectors(selectors || env.selectors),
targetValue = this.selector.elements[0].value;
tree.Extend.prototype = {
type: "Extend",
accept: function (visitor) {
this.selector = visitor.visit(this.ruleset);
},
eval: function (env, selectors) {
var selfSelectors = findSelfSelectors(selectors || env.selectors),
targetValue = this.selector.elements[0].value;
env.frames.forEach(function(frame) {
frame.rulesets().forEach(function(rule) {
rule.selectors.forEach(function(selector) {
selector.elements.forEach(function(element, idx) {
if (element.value === targetValue) {
selfSelectors.forEach(function(_selector) {
_selector.elements[0] = new tree.Element(
element.combinator,
_selector.elements[0].value,
_selector.elements[0].index
);
rule.selectors.push(new tree.Selector(
selector.elements
.slice(0, idx)
.concat(_selector.elements)
.concat(selector.elements.slice(idx + 1))
));
});
}
env.frames.forEach(function(frame) {
frame.rulesets().forEach(function(rule) {
rule.selectors.forEach(function(selector) {
selector.elements.forEach(function(element, idx) {
if (element.value === targetValue) {
selfSelectors.forEach(function(_selector) {
_selector.elements[0] = new tree.Element(
element.combinator,
_selector.elements[0].value,
_selector.elements[0].index
);
rule.selectors.push(new tree.Selector(
selector.elements
.slice(0, idx)
.concat(_selector.elements)
.concat(selector.elements.slice(idx + 1))
));
});
}
});
});
});
});
});
return this;
return this;
}
};
function findSelfSelectors(selectors) {
@@ -53,5 +59,4 @@ function findSelfSelectors(selectors) {
return ret;
}
})(require('../tree'));

View File

@@ -49,6 +49,11 @@ tree.Import = function (path, imports, features, once, index, rootpath) {
// ruleset.
//
tree.Import.prototype = {
type: "Import",
accept: function (visitor) {
this.features = visitor.visit(this.features);
this._path = visitor.visit(this._path);
},
toCSS: function (env) {
var features = this.features ? ' ' + this.features.toCSS(env) : '';

View File

@@ -6,6 +6,7 @@ tree.JavaScript = function (string, index, escaped) {
this.index = index;
};
tree.JavaScript.prototype = {
type: "JavaScript",
eval: function (env) {
var result,
that = this,

View File

@@ -2,8 +2,9 @@
tree.Keyword = function (value) { this.value = value };
tree.Keyword.prototype = {
eval: function () { return this },
toCSS: function () { return this.value },
type: "Keyword",
eval: function () { return this; },
toCSS: function () { return this.value; },
compare: function (other) {
if (other instanceof tree.Keyword) {
return other.value === this.value ? 0 : 1;

View File

@@ -8,6 +8,11 @@ tree.Media = function (value, features) {
this.ruleset.allowImports = true;
};
tree.Media.prototype = {
type: "Media",
accept: function (visitor) {
this.features = visitor.visit(this.features);
this.ruleset = visitor.visit(this.ruleset);
},
toCSS: function (ctx, env) {
var features = this.features.toCSS(env);

View File

@@ -9,6 +9,11 @@ tree.mixin.Call = function (elements, args, index, filename, important) {
this.important = important;
};
tree.mixin.Call.prototype = {
type: "MixinCall",
accept: function (visitor) {
this.selector = visitor.visit(this.selector);
this.arguments = visitor.visit(this.arguments);
},
eval: function (env) {
var mixins, mixin, args, rules = [], match = false, i, m, f, isRecursive, isOneFound;
@@ -90,11 +95,17 @@ tree.mixin.Definition = function (name, params, rules, condition, variadic) {
this.frames = [];
};
tree.mixin.Definition.prototype = {
toCSS: function () { return "" },
variable: function (name) { return this.parent.variable.call(this, name) },
variables: function () { return this.parent.variables.call(this) },
find: function () { return this.parent.find.apply(this, arguments) },
rulesets: function () { return this.parent.rulesets.apply(this) },
type: "MixinDefinition",
accept: function (visitor) {
this.params = visitor.visit(this.params);
this.rules = visitor.visit(this.rules);
this.condition = visitor.visit(this.condition);
},
toCSS: function () { return ""; },
variable: function (name) { return this.parent.variable.call(this, name); },
variables: function () { return this.parent.variables.call(this); },
find: function () { return this.parent.find.apply(this, arguments); },
rulesets: function () { return this.parent.rulesets.apply(this); },
evalParams: function (env, mixinEnv, args, evaldArguments) {
var frame = new(tree.Ruleset)(null, []),

View File

@@ -4,6 +4,10 @@ tree.Negative = function (node) {
this.value = node;
};
tree.Negative.prototype = {
type: "Negative",
accept: function (visitor) {
this.value = visitor.visit(this.value);
},
toCSS: function (env) {
return '-' + this.value.toCSS(env);
},

View File

@@ -5,34 +5,40 @@ tree.Operation = function (op, operands, isSpaced) {
this.operands = operands;
this.isSpaced = isSpaced;
};
tree.Operation.prototype.eval = function (env) {
var a = this.operands[0].eval(env),
b = this.operands[1].eval(env),
temp;
tree.Operation.prototype = {
type: "Operation",
accept: function (visitor) {
this.operands = visitor.visit(this.operands);
},
eval: function (env) {
var a = this.operands[0].eval(env),
b = this.operands[1].eval(env),
temp;
if (env.isMathsOn()) {
if (a instanceof tree.Dimension && b instanceof tree.Color) {
if (this.op === '*' || this.op === '+') {
temp = b, b = a, a = temp;
} else {
throw { type: "Operation",
message: "Can't substract or divide a color from a number" };
if (env.isMathsOn()) {
if (a instanceof tree.Dimension && b instanceof tree.Color) {
if (this.op === '*' || this.op === '+') {
temp = b, b = a, a = temp;
} else {
throw { type: "Operation",
message: "Can't substract or divide a color from a number" };
}
}
if (!a.operate) {
throw { type: "Operation",
message: "Operation on an invalid type" };
}
}
if (!a.operate) {
throw { type: "Operation",
message: "Operation on an invalid type" };
}
return a.operate(env, this.op, b);
} else {
return new(tree.Operation)(this.op, [a, b], this.isSpaced);
return a.operate(env, this.op, b);
} else {
return new(tree.Operation)(this.op, [a, b], this.isSpaced);
}
},
toCSS: function (env) {
var separator = this.isSpaced ? " " : "";
return this.operands[0].toCSS() + separator + this.op + separator + this.operands[1].toCSS();
}
};
tree.Operation.prototype.toCSS = function (env) {
var separator = this.isSpaced ? " " : "";
return this.operands[0].toCSS() + separator + this.op + separator + this.operands[1].toCSS();
};
tree.operate = function (env, op, a, b) {
switch (op) {

View File

@@ -5,6 +5,10 @@ tree.Paren = function (node) {
this.value = node;
};
tree.Paren.prototype = {
type: "Paren",
accept: function (visitor) {
this.value = visitor.visit(this.value);
},
toCSS: function (env) {
return '(' + this.value.toCSS(env).trim() + ')';
},

View File

@@ -7,6 +7,7 @@ tree.Quoted = function (str, content, escaped, i) {
this.index = i;
};
tree.Quoted.prototype = {
type: "Quoted",
toCSS: function () {
if (this.escaped) {
return this.value;

View File

@@ -11,39 +11,44 @@ tree.Rule = function (name, value, important, index, inline) {
this.variable = true;
} else { this.variable = false }
};
tree.Rule.prototype.toCSS = function (env) {
if (this.variable) { return "" }
else {
return this.name + (env.compress ? ':' : ': ') +
this.value.toCSS(env) +
this.important + (this.inline ? "" : ";");
}
};
tree.Rule.prototype.eval = function (env) {
var strictMathsBypass = false;
if (this.name === "font" && env.strictMaths === false) {
strictMathsBypass = true;
env.strictMaths = true;
}
try {
return new(tree.Rule)(this.name,
this.value.eval(env),
this.important,
this.index, this.inline);
}
finally {
if (strictMathsBypass) {
env.strictMaths = false;
tree.Rule.prototype = {
type: "Rule",
accept: function (visitor) {
this.value = visitor.visit(this.value);
},
toCSS: function (env) {
if (this.variable) { return "" }
else {
return this.name + (env.compress ? ':' : ': ') +
this.value.toCSS(env) +
this.important + (this.inline ? "" : ";");
}
},
eval: function (env) {
var strictMathsBypass = false;
if (this.name === "font" && env.strictMaths === false) {
strictMathsBypass = true;
env.strictMaths = true;
}
try {
return new(tree.Rule)(this.name,
this.value.eval(env),
this.important,
this.index, this.inline);
}
finally {
if (strictMathsBypass) {
env.strictMaths = false;
}
}
},
makeImportant: function () {
return new(tree.Rule)(this.name,
this.value,
"!important",
this.index, this.inline);
}
};
tree.Rule.prototype.makeImportant = function () {
return new(tree.Rule)(this.name,
this.value,
"!important",
this.index, this.inline);
};
})(require('../tree'));

View File

@@ -7,6 +7,11 @@ tree.Ruleset = function (selectors, rules, strictImports) {
this.strictImports = strictImports;
};
tree.Ruleset.prototype = {
type: "Ruleset",
accept: function (visitor) {
this.selectors = visitor.visit(this.selectors);
this.rules = visitor.visit(this.rules);
},
eval: function (env) {
var selectors = this.selectors && this.selectors.map(function (s) { return s.eval(env) });
var ruleset = new(tree.Ruleset)(selectors, this.rules.slice(0), this.strictImports);

View File

@@ -4,50 +4,56 @@ tree.Selector = function (elements, extend) {
this.elements = elements;
this.extend = extend;
};
tree.Selector.prototype.match = function (other) {
var elements = this.elements,
len = elements.length,
oelements, olen, max, i;
tree.Selector.prototype = {
type: "Selector",
accept: function (visitor) {
this.elements = visitor.visit(this.elements);
},
match: function (other) {
var elements = this.elements,
len = elements.length,
oelements, olen, max, i;
oelements = other.elements.slice(
(other.elements.length && other.elements[0].value === "&") ? 1 : 0);
olen = oelements.length;
max = Math.min(len, olen)
oelements = other.elements.slice(
(other.elements.length && other.elements[0].value === "&") ? 1 : 0);
olen = oelements.length;
max = Math.min(len, olen);
if (olen === 0 || len < olen) {
return false;
} else {
for (i = 0; i < max; i++) {
if (elements[i].value !== oelements[i].value) {
return false;
if (olen === 0 || len < olen) {
return false;
} else {
for (i = 0; i < max; i++) {
if (elements[i].value !== oelements[i].value) {
return false;
}
}
}
}
return true;
};
tree.Selector.prototype.eval = function (env) {
return new(tree.Selector)(this.elements.map(function (e) {
return e.eval(env);
}), this.extend);
};
tree.Selector.prototype.toCSS = function (env) {
if (this._css) { return this._css }
if (this.elements[0].combinator.value === "") {
this._css = ' ';
} else {
this._css = '';
}
this._css += this.elements.map(function (e) {
if (typeof(e) === 'string') {
return ' ' + e.trim();
return true;
},
eval: function (env) {
return new(tree.Selector)(this.elements.map(function (e) {
return e.eval(env);
}), this.extend);
},
toCSS: function (env) {
if (this._css) { return this._css }
if (this.elements[0].combinator.value === "") {
this._css = ' ';
} else {
return e.toCSS(env);
this._css = '';
}
}).join('');
return this._css;
this._css += this.elements.map(function (e) {
if (typeof(e) === 'string') {
return ' ' + e.trim();
} else {
return e.toCSS(env);
}
}).join('');
return this._css;
}
};
})(require('../tree'));

View File

@@ -4,6 +4,7 @@ tree.UnicodeDescriptor = function (value) {
this.value = value;
};
tree.UnicodeDescriptor.prototype = {
type: "UnicodeDescriptor",
toCSS: function (env) {
return this.value;
},

View File

@@ -5,6 +5,10 @@ tree.URL = function (val, rootpath) {
this.rootpath = rootpath;
};
tree.URL.prototype = {
type: "Url",
accept: function (visitor) {
this.value = visitor.visit(this.value);
},
toCSS: function () {
return "url(" + this.value.toCSS() + ")";
},

View File

@@ -2,9 +2,12 @@
tree.Value = function (value) {
this.value = value;
this.is = 'value';
};
tree.Value.prototype = {
type: "Value",
accept: function (visitor) {
this.value = visitor.visit(this.value);
},
eval: function (env) {
if (this.value.length === 1) {
return this.value[0].eval(env);

View File

@@ -2,6 +2,7 @@
tree.Variable = function (name, index, file) { this.name = name, this.index = index, this.file = file };
tree.Variable.prototype = {
type: "Variable",
eval: function (env) {
var variable, v, name = this.name;

44
lib/less/visitor.js Normal file
View File

@@ -0,0 +1,44 @@
(function (tree) {
tree.visitor = function(implementation) {
this._implementation = implementation;
};
tree.visitor.prototype = {
visit: function(node) {
if (node instanceof Array) {
return this.visitArray(node);
}
if (!node || !node.type) {
return node;
}
var funcName = "visit" + node.type,
func = this._implementation[funcName],
visitArgs;
if (func) {
visitArgs = {visitDeeper: true};
node = func(node);
}
if ((!visitArgs || visitArgs.visitDeeper) && node.accept) {
node.accept(this);
}
return node;
},
visitArray: function(nodes) {
var i, newNodes;
for(i = 0; i < nodes.length; i++) {
var evald = this.visit(nodes[i]);
if (evald instanceof Array) {
newNodes = newNodes.concat(evald);
} else {
newNodes.push(evald);
}
}
return newNodes;
}
};
})(require('./tree'));