From ad66408b9feed27fd02df68569915dd894e1ee51 Mon Sep 17 00:00:00 2001 From: cloudhead Date: Tue, 23 Feb 2010 13:39:05 -0500 Subject: [PATCH] init --- LICENSE | 0 README.md | 22 +++ lib/less.js | 2 + lib/less/node.js | 0 lib/less/node/call.js | 5 + lib/less/node/color.js | 39 ++++ lib/less/node/dimension.js | 30 +++ lib/less/node/directive.js | 16 ++ lib/less/node/element.js | 21 ++ lib/less/node/expression.js | 18 ++ lib/less/node/keyword.js | 5 + lib/less/node/operation.js | 8 + lib/less/node/quoted.js | 5 + lib/less/node/rule.js | 31 +++ lib/less/node/ruleset.js | 49 +++++ lib/less/node/selector.js | 12 ++ lib/less/node/value.js | 0 lib/less/node/variable.js | 18 ++ lib/less/parser.js | 374 ++++++++++++++++++++++++++++++++++++ 19 files changed, 655 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 lib/less.js create mode 100644 lib/less/node.js create mode 100644 lib/less/node/call.js create mode 100644 lib/less/node/color.js create mode 100644 lib/less/node/dimension.js create mode 100644 lib/less/node/directive.js create mode 100644 lib/less/node/element.js create mode 100644 lib/less/node/expression.js create mode 100644 lib/less/node/keyword.js create mode 100644 lib/less/node/operation.js create mode 100644 lib/less/node/quoted.js create mode 100644 lib/less/node/rule.js create mode 100644 lib/less/node/ruleset.js create mode 100644 lib/less/node/selector.js create mode 100644 lib/less/node/value.js create mode 100644 lib/less/node/variable.js create mode 100644 lib/less/parser.js diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..1bc7d0a --- /dev/null +++ b/README.md @@ -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 diff --git a/lib/less.js b/lib/less.js new file mode 100644 index 0000000..f47836f --- /dev/null +++ b/lib/less.js @@ -0,0 +1,2 @@ +var less = {}; + diff --git a/lib/less/node.js b/lib/less/node.js new file mode 100644 index 0000000..e69de29 diff --git a/lib/less/node/call.js b/lib/less/node/call.js new file mode 100644 index 0000000..97ce9be --- /dev/null +++ b/lib/less/node/call.js @@ -0,0 +1,5 @@ + +node.Call = function Call(name, args) { + this.name = name; + this.args = args; +}; diff --git a/lib/less/node/color.js b/lib/less/node/color.js new file mode 100644 index 0000000..b4ed6f6 --- /dev/null +++ b/lib/less/node/color.js @@ -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); + } +}; + diff --git a/lib/less/node/dimension.js b/lib/less/node/dimension.js new file mode 100644 index 0000000..501c18f --- /dev/null +++ b/lib/less/node/dimension.js @@ -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); + } +}; + diff --git a/lib/less/node/directive.js b/lib/less/node/directive.js new file mode 100644 index 0000000..ddecdf0 --- /dev/null +++ b/lib/less/node/directive.js @@ -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'; + } +}; diff --git a/lib/less/node/element.js b/lib/less/node/element.js new file mode 100644 index 0000000..e9923c3 --- /dev/null +++ b/lib/less/node/element.js @@ -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; + } +}; diff --git a/lib/less/node/expression.js b/lib/less/node/expression.js new file mode 100644 index 0000000..47f9288 --- /dev/null +++ b/lib/less/node/expression.js @@ -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(' '); +}; diff --git a/lib/less/node/keyword.js b/lib/less/node/keyword.js new file mode 100644 index 0000000..d6d5870 --- /dev/null +++ b/lib/less/node/keyword.js @@ -0,0 +1,5 @@ + +node.Keyword = function Keyword(value) { this.value = value }; +node.Keyword.prototype.toCSS = function () { + return this.value; +}; diff --git a/lib/less/node/operation.js b/lib/less/node/operation.js new file mode 100644 index 0000000..db526a1 --- /dev/null +++ b/lib/less/node/operation.js @@ -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)); +}; + diff --git a/lib/less/node/quoted.js b/lib/less/node/quoted.js new file mode 100644 index 0000000..99017e9 --- /dev/null +++ b/lib/less/node/quoted.js @@ -0,0 +1,5 @@ +node.Quoted = function Quoted(value) { this.value = value }; +node.Quoted.prototype.toCSS = function () { + var css = this.value; + return css; +}; diff --git a/lib/less/node/rule.js b/lib/less/node/rule.js new file mode 100644 index 0000000..6cd8c0c --- /dev/null +++ b/lib/less/node/rule.js @@ -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(', '); +}; + diff --git a/lib/less/node/ruleset.js b/lib/less/node/ruleset.js new file mode 100644 index 0000000..3a0b3a5 --- /dev/null +++ b/lib/less/node/ruleset.js @@ -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(''); + } +}; + diff --git a/lib/less/node/selector.js b/lib/less/node/selector.js new file mode 100644 index 0000000..a343e4b --- /dev/null +++ b/lib/less/node/selector.js @@ -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(''); +}; + diff --git a/lib/less/node/value.js b/lib/less/node/value.js new file mode 100644 index 0000000..e69de29 diff --git a/lib/less/node/variable.js b/lib/less/node/variable.js new file mode 100644 index 0000000..220cc6e --- /dev/null +++ b/lib/less/node/variable.js @@ -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 } + } + } + } +}; + diff --git a/lib/less/parser.js b/lib/less/parser.js new file mode 100644 index 0000000..afe067f --- /dev/null +++ b/lib/less/parser.js @@ -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; + } + } +};