This commit is contained in:
cloudhead 2010-02-23 13:39:05 -05:00
commit ad66408b9f
19 changed files with 655 additions and 0 deletions

0
LICENSE Normal file
View File

22
README.md Normal file
View File

@ -0,0 +1,22 @@
less.js
=======
> Leaner CSS, in your browser.
development status
------------------
### Implemented features:
- Variables
- Nested rules
- & selector
- Numerical operations
### Not yet implemented:
- Mixins
- Color operations
- Importing
- Namespaces
- Accessors

2
lib/less.js Normal file
View File

@ -0,0 +1,2 @@
var less = {};

0
lib/less/node.js Normal file
View File

5
lib/less/node/call.js Normal file
View File

@ -0,0 +1,5 @@
node.Call = function Call(name, args) {
this.name = name;
this.args = args;
};

39
lib/less/node/color.js Normal file
View File

@ -0,0 +1,39 @@
//
// RGB Colors - #ff0014, #eee
//
node.Color = function Color(val) {
if (val.length == 6) {
this.value = val.match(/.{2}/g).map(function (c) {
return parseInt(c, 16);
});
} else {
this.value = val.split('').map(function (c) {
return parseInt(c + c, 16);
});
}
};
node.Color.prototype = {
eval: function () { return this },
toCSS: function () {
return '#' + this.value.map(function (i) {
return i.toString(16);
}).join('');
},
'+': function (other) {
return new(node.Color)
(this.value + other.value, this.unit);
},
'-': function (other) {
return new(node.Color)
(this.value - other.value, this.unit);
},
'*': function (other) {
return new(node.Color)
(this.value * other.value, this.unit);
},
'/': function (other) {
return new(node.Color)
(this.value / other.value, this.unit);
}
};

View File

@ -0,0 +1,30 @@
node.Dimension = function Dimension(value, unit) {
this.value = parseFloat(value);
this.unit = unit || null;
};
node.Dimension.prototype = {
eval: function () { return this },
toCSS: function () {
var css = this.value + this.unit;
return css;
},
'+': function (other) {
return new(node.Dimension)
(this.value + other.value, this.unit);
},
'-': function (other) {
return new(node.Dimension)
(this.value - other.value, this.unit);
},
'*': function (other) {
return new(node.Dimension)
(this.value * other.value, this.unit);
},
'/': function (other) {
return new(node.Dimension)
(this.value / other.value, this.unit);
}
};

View File

@ -0,0 +1,16 @@
node.Directive = function Directive(name, value) {
this.name = name;
if (Array.isArray(value)) {
this.rules = value;
} else {
this.value = value;
}
};
node.Directive.prototype.toCSS = function () {
if (this.rules) {
} else {
return this.name + ' ' + this.value.toCSS() + ';\n';
}
};

21
lib/less/node/element.js Normal file
View File

@ -0,0 +1,21 @@
node.Element = function Element(combinator, value) {
this.combinator = combinator;
this.value = value.trim();
};
node.Element.prototype.toCSS = function () {
var css = this.combinator.toCSS() + this.value;
return css;
};
node.Combinator = function Combinator(value) {
this.value = value.trim();
};
node.Combinator.prototype.toCSS = function () {
switch (this.value) {
case '&': return "";
case ':': return ' :';
case '>': return ' > ';
default: return ' ' + this.value;
}
};

View File

@ -0,0 +1,18 @@
node.Expression = function Expression(value) { this.value = value };
node.Expression.prototype.eval = function (env) {
if (this.value.length > 1) {
throw new(Error)("can't eval compound expression");
} else {
return this.value[0].eval(env);
}
};
node.Expression.prototype.toCSS = function (env) {
var evaled;
evaled = this.value.map(function (e) {
if (e.eval) {
e = e.eval(env);
}
return e.toCSS ? e.toCSS(env) : e;
});
return evaled.join(' ');
};

5
lib/less/node/keyword.js Normal file
View File

@ -0,0 +1,5 @@
node.Keyword = function Keyword(value) { this.value = value };
node.Keyword.prototype.toCSS = function () {
return this.value;
};

View File

@ -0,0 +1,8 @@
node.Operation = function Operation(op, operands) {
this.op = op.trim();
this.operands = operands;
};
node.Operation.prototype.eval = function (env) {
return this.operands[0].eval(env)[this.op](this.operands[1].eval(env));
};

5
lib/less/node/quoted.js Normal file
View File

@ -0,0 +1,5 @@
node.Quoted = function Quoted(value) { this.value = value };
node.Quoted.prototype.toCSS = function () {
var css = this.value;
return css;
};

31
lib/less/node/rule.js Normal file
View File

@ -0,0 +1,31 @@
node.Rule = function Rule(name, value) {
this.name = name;
this.value = value;
if (name.charAt(0) === '@') {
require('sys').puts('NEW VAR, value:' + require('sys').inspect(value))
this.variable = true;
} else { this.variable = false }
};
node.Rule.prototype.toCSS = function (env) {
return this.name + ": " + (this.value.toCSS ? this.value.toCSS(env) : this.value) + ";";
};
node.Value = function Value(value) {
this.value = value;
this.is = 'value';
};
node.Value.prototype.eval = function (env) {
if (this.value.length === 1) {
return this.value[0].eval(env);
} else {
throw new(Error)("trying to evaluate compound value");
}
};
node.Value.prototype.toCSS = function (env) {
return this.value.map(function (e) {
return e.toCSS ? e.toCSS(env) : e;
}).join(', ');
};

49
lib/less/node/ruleset.js Normal file
View File

@ -0,0 +1,49 @@
node.Ruleset = function Ruleset(selectors, rules) {
this.selectors = selectors;
this.rules = rules;
};
node.Ruleset.prototype = {
variables: function () {
return this.rules.filter(function (r) {
if (r instanceof node.Rule && r.variable === true) { return r }
});
},
toCSS: function (path, env) {
var css = [], rules = [], rulesets = [];
if (! this.root) path.push(this.selectors.map(function (s) { return s.toCSS(env) }));
env.frames.unshift(this);
for (var i = 0; i < this.rules.length; i++) {
if (this.rules[i] instanceof node.Ruleset) { continue }
if (this.rules[i].toCSS) {
rules.push(this.rules[i].toCSS(env));
} else {
if (this.rules[i].value) {
rules.push(this.rules[i].value.toString());
}
}
}
for (var i = 0; i < this.rules.length; i++) {
if (! (this.rules[i] instanceof node.Ruleset)) { continue }
rulesets.push(this.rules[i].toCSS(path, env));
}
if (rules.length > 0) {
if (path.length > 0) {
css.push(path.join('').trim(),
" {\n " + rules.join('\n ') + "\n}\n",
rulesets.join(''));
} else {
css.push(rules.join('\n'), rulesets.join(''));
}
}
path.pop();
env.frames.shift();
return css.join('');
}
};

12
lib/less/node/selector.js Normal file
View File

@ -0,0 +1,12 @@
node.Selector = function Selector(elements) { this.elements = elements };
node.Selector.prototype.toCSS = function () {
return this.elements.map(function (e) {
if (typeof(e) === 'string') {
return ' ' + e.trim();
} else {
return e.toCSS();
}
}).join('');
};

0
lib/less/node/value.js Normal file
View File

18
lib/less/node/variable.js Normal file
View File

@ -0,0 +1,18 @@
node.Variable = function Variable(name) { this.name = name };
node.Variable.prototype.eval = function (env) {
var variables, variable;
for (var i = 0; i < env.frames.length; i++) {
variables = env.frames[i].variables();
for (var j = 0; j < variables.length; j++) {
variable = variables[j];
if (variable.name === this.name) {
if (variable.value.eval) {
return variable.value.eval(env);
} else { return variable.value }
}
}
}
};

374
lib/less/parser.js Normal file
View File

@ -0,0 +1,374 @@
var less = exports || {};
var input, // LeSS input string
i = 0, // current index in `input`
j = 0, // current chunk
chunks = [], // chunkified input
current = 0, // index of current chunk, in `input`
inputLength;
function peek(regex) {
var match;
regex.lastIndex = i;
if ((match = regex.exec(input)) &&
(regex.lastIndex - match[0].length === i)) {
return match;
}
}
//
// Parse from a token or regexp, and move forward if match
//
function $(tok, root) {
var match, args, length, c, index;
// Non-terminal
if (tok instanceof Function) {
return tok.call(less.parser.parsers, root);
// Terminal
} else if (typeof(tok) === 'string') {
match = input[i] === tok ? tok : null;
length = 1;
} else {
if (i > current + chunks[j].length) {
current += chunks[j++].length;
}
tok.lastIndex = index = i - current;
match = tok.exec(chunks[j]);
if (match) {
length = match[0].length;
if (tok.lastIndex - length !== index) { return }
}
}
if (match) {
i += length;
while (i < inputLength) {
c = input.charCodeAt(i);
if (! (c === 32 || c === 10)) { break }
i++;
}
return match.length === 1 ? match[0] : match;
}
}
less.parser = {
parse: function (str) {
var tree;
input = str;
inputLength = input.length;
chunks = input.split(/\n\n/g);
for (var k = 0; k < chunks.length; k++) {
if (k < chunks.length - 1) { chunks[k] += '\n' }
if (k) { chunks[k] = '\n' + chunks[k] }
}
// Start with the primary rule
tree = new(node.Ruleset)([], $(this.parsers.primary, []));
tree.root = true;
if (i < input.length - 1) {
throw new(Error)("Parse Error: " + input.slice(0, i));
}
return tree;
},
parsers: {
entities: {
string: function string() {
var str;
if (input[i] !== '"' && input[i] !== "'") return;
if (str = $(/"(?:[^"\\\r\n]|\\.)*"|'(?:[^'\\\r\n]|\\.)*'/g)) {
return new(node.Quoted)(str);
}
},
keyword: function keyword() {
var k;
if (k = $(/[a-z-]+/g)) { return new(node.Keyword)(k) }
},
call: function call() {
var name, args;
if (! (name = $(/([a-zA-Z0-9_-]+)\(/g))) return;
args = $(this.entities.arguments);
if (! $(')')) return;
if (name) { return new(node.Call)(name[1], args) }
},
arguments: function arguments() {
var args = [], arg;
while (arg = $(this.expression)) {
args.push(arg);
if (! $(',')) { break }
}
return args;
},
accessor: function accessor() {
},
literal: function literal() {
return $(this.entities.dimension) ||
$(this.entities.color) ||
$(this.entities.string);
},
url: function url() {
},
font: function font() {
},
variable: function variable(def) {
var name;
if (input[i] !== '@') return;
if (def && (name = $(/(@[a-zA-Z0-9_-]+)\s*:/g))) { return name[1] }
else if (!def && (name = $(/@[a-zA-Z0-9_-]+/g))) { return new(node.Variable)(name) }
},
color: function color() {
var rgb;
if (input[i] !== '#') return;
if (rgb = $(/#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})/g)) {
return new(node.Color)(rgb[1]);
}
},
dimension: function dimension() {
var number, unit;
number = $(/-?[0-9]*\.?[0-9]+/g);
unit = $(/(?:px|%|em|pc|ex|in|deg|s|pt|cm|mm)/g);
if (number) { return new(node.Dimension)(number, unit) }
}
},
mixin: {
call: function mixinCall() {
var prefix, mixin;
if (input[i] !== '.') return;
i++;
mixin = $(this.entities.call);
if (mixin && $(';')) {
return ['MIXIN-CALL', mixin];
}
},
definition: function mixinDefinition(root) {
var name, params = [], match, ruleset, param, value;
if (input[i] !== '.' || peek(/[^{]*(;|})/g)) return;
if (match = $(/([#.][a-zA-Z0-9_-]+)\s*\(/g)) {
name = match[1];
while (param = $(this.entities.variable)) {
value = null;
if ($(':')) {
if (value = $(this.expression)) {
params.push([param, value]);
} else {
throw new(Error)("Expected value");
}
} else {
params.push([param, null]);
}
if (! $(',')) { break }
}
if (! $(')')) throw new(Error)("Expected )");
ruleset = $(this.block, root);
if (ruleset) {
return ['MIXIN-DEF', name, params, ruleset];
}
}
}
},
entity: function entity() {
var entities = [
"url", "variable", "call", "accessor",
"keyword", "literal", "font"
], e;
for (var i = 0; i < entities.length; i++) {
if (e = $(this.entities[entities[i]])) {
return e;
}
}
},
combinator: function combinator() {
var match;
if (match = $(/[+>~]/g) || $('&') || $(/::/g)) {
return new(node.Combinator)(match);
}
},
selector: function selector() {
var sel, e, elements = [], match;
while (e = $(this.element)) { elements.push(e) }
if (elements.length > 0) { return new(node.Selector)(elements) }
},
element: function element() {
var e, t;
c = $(this.combinator);
e = $(/[.#:]?[a-zA-Z0-9_-]+/g) || $('*') || /*$(this.attribute) ||*/ $(/\([a-z0-9+-]+\)/g);
if (e) { return new(node.Element)(c, e) }
},
tag: function tag() {
return $(/[a-zA-Z][a-zA-Z-]*[0-9]?/g) || $('*');
},
attribute: function attribute() {
var attr = '', key, val, op;
if (! $('[')) return;
if ((key = $(this.tag)) &&
(op = $(/[|~*$^]?=/g)) &&
(val = $(this.entities.string) || $(/[a-zA-Z0-9_-]+/g))) {
attr = [key, op, val];
} else if (val = $(this.tag) || $(this.string)) {
attr = val;
}
if (! $(']')) return;
if (attr) { return ['ATTR', '[' + attr + ']'] }
},
block: function block(node) {
var content;
if ($('{') && (content = $(this.primary, node)) && $('}')) {
return content;
}
},
ruleset: function ruleset(root) {
var selectors = [], s, rules, match;
if (peek(/[^{]+[;}]/g)) return;
if (match = peek(/([a-z.#: _-]+)[\s\n]*\{/g)) {
i += match[0].length - 1;
selectors = [new(node.Selector)([match[1]])];
} else {
while (s = $(this.selector)) {
selectors.push(s);
if (! $(',')) { break }
}
}
rules = $(this.block, root);
if (selectors.length > 0 && rules) {
return new(node.Ruleset)(selectors, rules);
}
},
rule: function rule() {
var name, value, match;
if (name = $(this.property) || $(this.entities.variable, true)) {
if ((name[0] != '@') && (match =
peek(/((?:[\s\w."']|-[a-z])+|[^@+\/*(-;}]+)[;}][\s\n]*/g))) {
i += match[0].length;
return new(node.Rule)(name, match[1]);
}
if ((value = $(this.value)) && $(';')) {
return new(node.Rule)(name, value);
}
}
},
directive: function directive(root) {
var name, value, rules;
if (input[i] !== '@') return;
if (name = $(/@[a-z]+/g)) {
if (name === '@media' || name === '@font-face') {
if (rules = $(this.block, root)) {
return new(node.Directive)(name, rules);
}
} else if ((value = $(this.entity)) && $(';')) {
return new(node.Directive)(name, value);
}
}
},
value: function value() {
var e, expressions = [];
while (e = $(this.expression)) {
expressions.push(e);
if (! $(',')) { break }
}
if (expressions.length > 0) {
return new(node.Value)(expressions);
}
},
sub: function sub() {
var e;
if ($('(') && (e = $(this.expression)) && $(')')) {
return ["()", e];
}
},
multiplication: function () {
var m, a, op;
if (m = $(this.operand)) {
if ((op = $(/[\/*]/g)) && (a = $(this.multiplication))) {
return new(node.Operation)(op, [m, a]);
} else {
return m;
}
}
},
addition: function () {
var m, a, op;
if (m = $(this.multiplication)) {
if ((op = $(/[-+]\s+/g)) && (a = $(this.addition))) {
return new(node.Operation)(op, [m, a]);
} else {
return m;
}
}
},
operand: function () {
var o;
if (o = $(this.sub) || $(this.entities.dimension) ||
$(this.entities.color) || $(this.entities.variable) ||
($('-') && $(this.operand))) {
return o;
}
},
expression: function expression() {
var e, delim, entities = [], d;
while (e = $(this.addition) || $(this.entity)) {
entities.push(e);
}
if (entities.length > 0) {
return new(node.Expression)(entities);
}
},
property: function property() {
var name;
if (name = $(/(-?[-a-z]+)\s*:/g)) {
return name[1];
}
},
primary: function primary(root) {
var node;
while (node = $(this.ruleset, []) || $(this.rule) || $(this.mixin.definition, []) ||
$(this.mixin.call) || $(/\/\*([^*]|\*+[^\/*])*\*+\//g) || $(/[\n\s]+/g) || $(this.directive)) {
root.push(node);
}
return root;
}
}
};