carto/lib/less/parser.js

517 lines
17 KiB
JavaScript
Raw Normal View History

if (typeof(window) === 'undefined') {
var less = exports || {};
var tree = require(require('path').join(__dirname, '..', 'less', 'tree'));
}
2010-02-24 12:19:31 +08:00
//
// less.js - parser
//
// A relatively straight-forward recursive-descent parser.
// There is no tokenization/lexing stage, the input is parsed
// in one sweep.
//
// To make the parser fast enough to run in the browser, several
// optimization had to be made:
//
// - Instead of the more commonly used technique of slicing the
// input string on every match, we use global regexps (/g),
// and move the `lastIndex` pointer on match, foregoing `slice()`
// completely. This gives us a 3x speed-up.
//
// - Matching on a huge input is often cause of slowdowns,
// especially with the /g flag. The solution to that is to
// chunkify the input: we split it by /\n\n/, just to be on
// the safe side. The chunks are stored in the `chunks` var,
// `j` holds the current chunk index, and `current` holds
// the index of the current chunk in relation to `input`.
// This gives us an almost 4x speed-up.
//
// - In many cases, we don't need to match individual tokens;
// for example, if a value doesn't hold any variables, operations
// or dynamic references, the parser can effectively 'skip' it,
// treating it as a literal.
// An example would be '1px solid #000' - which evaluates to itself,
// we don't need to know what the individual components are.
// The drawback, of course is that you don't get the benefits of
// syntax-checking on the CSS. This gives us a 50% speed-up in the parser,
// and a smaller speed-up in the code-gen.
//
//
// Token matching is done with the `$` function, which either takes
// a terminal string or regexp, or a non-terminal function to call.
// It also takes care of moving all the indices forwards.
//
//
2010-02-24 02:39:05 +08:00
var input, // LeSS input string
2010-02-26 04:49:01 +08:00
i, // current index in `input`
j, // current chunk
chunks, // chunkified input
2010-02-26 04:49:01 +08:00
current, // index of current chunk, in `input`
2010-02-24 02:39:05 +08:00
inputLength;
function peek(regex) {
var match;
regex.lastIndex = i;
if ((match = regex.exec(input)) &&
(regex.lastIndex - match[0].length === i)) {
return match;
}
}
//
2010-02-24 12:19:31 +08:00
// Parse from a token, regexp or string, and move forward if match
2010-02-24 02:39:05 +08:00
//
function $(tok, root) {
2010-02-27 03:47:25 +08:00
var match, args, length, c, index, endIndex;
2010-02-24 12:19:31 +08:00
//
2010-02-24 02:39:05 +08:00
// Non-terminal
2010-02-24 12:19:31 +08:00
//
2010-02-24 02:39:05 +08:00
if (tok instanceof Function) {
return tok.call(less.parser.parsers, root);
2010-02-24 12:19:31 +08:00
//
2010-02-24 02:39:05 +08:00
// Terminal
2010-02-24 12:19:31 +08:00
//
// Either match a single character in the input,
// or match a regexp in the current chunk (chunk[j]).
//
2010-02-24 02:39:05 +08:00
} else if (typeof(tok) === 'string') {
match = input[i] === tok ? tok : null;
length = 1;
2010-02-24 12:19:31 +08:00
// 1. We move to the next chunk, if necessary.
// 2. Set the `lastIndex` to be relative
// to the current chunk, and try to match in it.
// 3. Make sure we matched at `index`. Because we use
// the /g flag, the match could be anywhere in the
// chunk. We have to make sure it's at our previous
// index, which we stored in [2].
//
2010-02-24 02:39:05 +08:00
} else {
2010-02-27 03:47:25 +08:00
if (i >= current + chunks[j].length &&
j < chunks.length - 1) { // 1.
2010-02-24 02:39:05 +08:00
current += chunks[j++].length;
}
2010-02-24 12:19:31 +08:00
tok.lastIndex = index = i - current; // 2.
match = tok.exec(chunks[j]);
2010-02-24 02:39:05 +08:00
if (match) {
length = match[0].length;
2010-02-24 12:19:31 +08:00
if (tok.lastIndex - length !== index) { return } // 3.
2010-02-24 02:39:05 +08:00
}
}
2010-02-24 12:19:31 +08:00
// The match is confirmed, add the match length to `i`,
// and consume any extra white-space characters (' ' || '\n')
// which come after that. The reason for this is that LeSS's
// grammar is mostly white-space insensitive.
//
2010-02-24 02:39:05 +08:00
if (match) {
i += length;
2010-02-27 03:47:25 +08:00
endIndex = current + chunks[j].length;
2010-02-24 02:39:05 +08:00
2010-02-27 03:47:25 +08:00
while (i <= endIndex) {
2010-02-24 02:39:05 +08:00
c = input.charCodeAt(i);
2010-02-27 03:47:32 +08:00
if (! (c === 32 || c === 10 || c === 9)) { break }
2010-02-24 02:39:05 +08:00
i++;
}
return match.length === 1 ? match[0] : match;
}
}
less.parser = {
optimization: 2,
2010-02-24 12:19:31 +08:00
//
// Parse an input string into an abstract syntax tree
//
2010-02-24 02:39:05 +08:00
parse: function (str) {
var root, start, end, zone, line, buff = [], c;
2010-02-24 12:20:38 +08:00
2010-02-26 04:49:01 +08:00
i = j = current = 0;
chunks = [];
2010-02-27 03:50:26 +08:00
input = str.replace(/\r\n/g, '\n');
2010-02-24 02:39:05 +08:00
inputLength = input.length;
2010-02-28 14:07:13 +08:00
this.error = null;
2010-02-24 02:39:05 +08:00
2010-02-24 12:20:38 +08:00
// Split the input into chunks,
// Either delimited by /\n\n/ or
// delmited by '\n}' (see rationale above),
// depending on the level of optimization.
if (this.optimization > 0) {
if (this.optimization > 2) {
input = input.replace(/\/\*(?:[^*]|\*+[^\/*])*\*+\//g, '');
chunks = input.split(/^(?=\n)/mg);
} else {
for (var k = 0; k < input.length; k++) {
if ((c = input.charAt(k)) === '}' && input.charCodeAt(k - 1) === 10) {
chunks.push(buff.concat('}').join(''));
buff = [];
} else {
buff.push(c);
}
}
chunks.push(buff.join(''));
}
} else {
chunks = [input];
}
2010-02-24 02:39:05 +08:00
// Start with the primary rule
root = new(tree.Ruleset)([], $(this.parsers.primary, []));
root.root = true;
2010-02-24 02:39:05 +08:00
2010-02-26 04:49:01 +08:00
// If `i` is smaller than the input length - 1,
// it means the parser wasn't able to parse the whole
// string, so we've got a parsing error.
2010-02-24 02:39:05 +08:00
if (i < input.length - 1) {
2010-02-26 04:49:01 +08:00
start = (function () {
for (var n = i; n > 0; n--) {
if (input[n] === '\n') { break }
}
return n;
})() + 1;
line = (input.slice(0, i).match(/\n/g) || "").length + 1;
2010-02-26 04:49:01 +08:00
end = input.slice(i).indexOf('\n') + i;
zone = stylize(input.slice(start, i), 'green') +
stylize(input.slice(i, end), 'yellow') + '\033[0m\n';
2010-02-26 04:49:01 +08:00
this.error = { name: "ParseError", message: "Parse Error on line " + line + ":\n" + zone };
2010-02-24 02:39:05 +08:00
}
return root;
2010-02-24 02:39:05 +08:00
},
parsers: {
2010-02-24 12:21:07 +08:00
primary: function primary(root) {
var node;
while (node = $(this.ruleset, []) || $(this.rule) || $(this.mixin.definition, []) ||
$(this.mixin.call) || $(/\/\*(?:[^*]|\*+[^\/*])*\*+\//g) || $(/\/\/.*/g) ||
$(/[\n\s]+/g) || $(this.directive, [])) {
2010-02-24 12:21:07 +08:00
root.push(node);
}
return root;
},
2010-02-24 02:39:05 +08:00
entities: {
string: function string() {
var str;
if (input[i] !== '"' && input[i] !== "'") return;
if (str = $(/"(?:[^"\\\r\n]|\\.)*"|'(?:[^'\\\r\n]|\\.)*'/g)) {
return new(tree.Quoted)(str);
2010-02-24 02:39:05 +08:00
}
},
keyword: function keyword() {
var k;
if (k = $(/[A-Za-z-]+/g)) { return new(tree.Keyword)(k) }
2010-02-24 02:39:05 +08:00
},
call: function call() {
var name, args;
if (! (name = $(/([a-zA-Z0-9_-]+)\(/g))) return;
2010-02-27 03:47:59 +08:00
if (name[1].toLowerCase() === 'alpha') { return $(this.alpha) }
2010-02-26 11:37:03 +08:00
2010-02-24 02:39:05 +08:00
args = $(this.entities.arguments);
if (! $(')')) return;
if (name) { return new(tree.Call)(name[1], args) }
2010-02-24 02:39:05 +08:00
},
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() {
2010-02-26 10:27:23 +08:00
var value;
if (! $(/url\(/g)) return;
value = $(this.entities.string) || $(/[-a-zA-Z0-9_%@$\/.&=:;#+?]+/g);
if (! $(')')) throw new(Error)("missing closing ) for url()");
return new(tree.URL)(value);
2010-02-24 02:39:05 +08:00
},
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(tree.Variable)(name) }
2010-02-24 02:39:05 +08:00
},
color: function color() {
var rgb;
if (input[i] !== '#') return;
if (rgb = $(/#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})/g)) {
return new(tree.Color)(rgb[1]);
2010-02-24 02:39:05 +08:00
}
},
dimension: function dimension() {
var value;
2010-02-24 12:21:17 +08:00
if (value = $(/(-?[0-9]*\.?[0-9]+)(px|%|em|pc|ex|in|deg|s|ms|pt|cm|mm)?/g)) {
return new(tree.Dimension)(value[1], value[2]);
}
2010-02-24 02:39:05 +08:00
}
},
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)) {
2010-02-24 12:21:17 +08:00
params.push([param, value]);
2010-02-24 02:39:05 +08:00
} 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 = [
2010-02-24 12:21:17 +08:00
"url", "variable", "call", "accessor",
2010-02-24 02:39:05 +08:00
"keyword", "literal", "font"
], e;
for (var i = 0; i < entities.length; i++) {
if (e = $(this.entities[entities[i]])) {
return e;
}
}
},
2010-02-26 11:32:13 +08:00
alpha: function alpha() {
var value;
2010-02-27 03:47:59 +08:00
if (! $(/opacity=/gi)) return;
2010-02-26 11:32:13 +08:00
if (value = $(/[0-9]+/g) || $(this.entities.variable)) {
if (! $(')')) throw new(Error)("missing closing ) for alpha()");
return new(tree.Alpha)(value);
2010-02-26 11:32:13 +08:00
}
},
2010-02-24 02:39:05 +08:00
combinator: function combinator() {
var match;
if (match = $(/[+>~]/g) || $('&') || $(/::/g)) {
return new(tree.Combinator)(match);
} else {
return new(tree.Combinator);
2010-02-24 02:39:05 +08:00
}
},
selector: function selector() {
var sel, e, elements = [], match;
while (e = $(this.element)) { elements.push(e) }
if (elements.length > 0) { return new(tree.Selector)(elements) }
2010-02-24 02:39:05 +08:00
},
element: function element() {
var e, t;
c = $(this.combinator);
2010-02-27 00:28:34 +08:00
e = $(/[.#:]?[a-zA-Z0-9_-]+/g) || $('*') || $(this.attribute) || $(/\([^)]*\)/g);
2010-02-24 02:39:05 +08:00
if (e) { return new(tree.Element)(c, e) }
2010-02-24 02:39:05 +08:00
},
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 = $(/[a-z]+/g) || $(this.entities.string)) {
if ((op = $(/[|~*$^]?=/g)) &&
(val = $(this.entities.string) || $(/[\w-]+/g))) {
attr = [key, op, val].join('');
} else { attr = key }
2010-02-24 02:39:05 +08:00
}
2010-02-24 02:39:05 +08:00
if (! $(']')) return;
2010-02-24 12:21:17 +08:00
2010-02-26 10:28:39 +08:00
if (attr) { return "[" + attr + "]" }
2010-02-24 02:39:05 +08:00
},
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(tree.Selector)([match[1]])];
2010-02-24 02:39:05 +08:00
} else {
while (s = $(this.selector)) {
selectors.push(s);
if (! $(',')) { break }
}
}
rules = $(this.block, root);
if (selectors.length > 0 && rules) {
return new(tree.Ruleset)(selectors, rules);
2010-02-24 02:39:05 +08:00
}
},
rule: function rule() {
var name, value, match;
if (name = $(this.property) || $(this.entities.variable, true)) {
if ((name[0] != '@') && (match =
2010-02-27 03:48:32 +08:00
peek(/((?:[\s\w."']|-[a-z])+|[^@+\/*(;{}-]*)[;][\s\n]*/g))) {
2010-02-24 02:39:05 +08:00
i += match[0].length;
return new(tree.Rule)(name, match[1]);
2010-02-27 03:49:07 +08:00
} else if ((value = $(this.value)) && ($(';') || peek(/\}/g))) {
return new(tree.Rule)(name, value);
2010-02-24 02:39:05 +08:00
}
}
},
directive: function directive(root) {
var name, value, rules, types;
2010-02-24 02:39:05 +08:00
if (input[i] !== '@') return;
if (name = $(/@media|@page/g)) {
types = $(/[a-z:, ]+/g);
if (rules = $(this.block, root)) {
return new(tree.Directive)(name + " " + types, rules);
}
} else if (name = $(/@[-a-z]+/g)) {
if (name === '@font-face') {
2010-02-24 02:39:05 +08:00
if (rules = $(this.block, root)) {
return new(tree.Directive)(name, rules);
2010-02-24 02:39:05 +08:00
}
} else if ((value = $(this.entity)) && $(';')) {
return new(tree.Directive)(name, value);
2010-02-24 02:39:05 +08:00
}
}
},
value: function value() {
2010-02-26 10:29:34 +08:00
var e, expressions = [], important;
2010-02-24 02:39:05 +08:00
while (e = $(this.expression)) {
expressions.push(e);
if (! $(',')) { break }
}
2010-02-26 10:29:34 +08:00
important = $(/!\s*important/g);
2010-02-24 02:39:05 +08:00
if (expressions.length > 0) {
return new(tree.Value)(expressions, important);
2010-02-24 02:39:05 +08:00
}
},
sub: function sub() {
var e;
if ($('(') && (e = $(this.expression)) && $(')')) {
return e;
2010-02-24 02:39:05 +08:00
}
},
multiplication: function () {
var m, a, op;
if (m = $(this.operand)) {
if ((op = $(/[\/*]/g)) && (a = $(this.multiplication))) {
return new(tree.Operation)(op, [m, a]);
2010-02-24 02:39:05 +08:00
} else {
return m;
}
}
},
addition: function () {
var m, a, op;
if (m = $(this.multiplication)) {
if ((op = $(/[-+]\s+/g)) && (a = $(this.addition))) {
return new(tree.Operation)(op, [m, a]);
2010-02-24 02:39:05 +08:00
} else {
return m;
}
}
},
operand: function () {
var o;
if (o = $(this.sub) || $(this.entities.dimension) ||
$(this.entities.color) || $(this.entities.variable) ||
($('-') && $(this.operand))) {
2010-02-24 12:21:17 +08:00
return o;
2010-02-24 02:39:05 +08:00
}
},
expression: function expression() {
var e, delim, entities = [], d;
while (e = $(this.addition) || $(this.entity)) {
entities.push(e);
}
if (entities.length > 0) {
return new(tree.Expression)(entities);
2010-02-24 02:39:05 +08:00
}
},
property: function property() {
var name;
2010-02-26 23:54:48 +08:00
if (name = $(/(\*?-?[-a-z]+)\s*:/g)) {
2010-02-24 02:39:05 +08:00
return name[1];
}
}
}
};
2010-02-24 12:21:07 +08:00
2010-02-26 04:49:01 +08:00
// Stylize a string
function stylize(str, style) {
var styles = {
'bold' : [1, 22],
'underline' : [4, 24],
'yellow' : [33, 39],
'green' : [32, 39],
'red' : [31, 39]
};
return '\033[' + styles[style][0] + 'm' + str +
'\033[' + styles[style][1] + 'm';
}