carto
Version:
Mapnik Stylesheet Compiler
784 lines (691 loc) • 29.3 kB
JavaScript
var carto = exports,
tree = require('./tree'),
chroma = require('chroma-js'),
util = require('./util');
// 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.
carto.Parser = function Parser(env) {
var input, // LeSS input string
i, // current index in `input`
j, // current chunk
temp, // temporarily holds a chunk's state, for backtracking
memo, // temporarily holds `i`, when backtracking
chunks, // chunkified input
current, // index of current chunk, in `input`
parser;
var that = this;
function save() {
temp = chunks[j];
memo = i;
current = i;
}
function restore() {
chunks[j] = temp;
i = memo;
current = i;
}
function sync() {
if (i > current) {
chunks[j] = chunks[j].slice(i - current);
current = i;
}
}
//
// Parse from a token, regexp or string, and move forward if match
//
function $(tok) {
var match, length, c, endIndex;
// Non-terminal
if (tok instanceof Function) {
return tok.call(parser.parsers);
// Terminal
// Either match a single character in the input,
// or match a regexp in the current chunk (chunk[j]).
} else if (typeof(tok) === 'string') {
match = input.charAt(i) === tok ? tok : null;
length = 1;
sync();
} else {
sync();
match = tok.exec(chunks[j]);
if (match) {
length = match[0].length;
} else {
return null;
}
}
// 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.
if (match) {
var mem = i += length;
endIndex = i + chunks[j].length - length;
while (i < endIndex) {
c = input.charCodeAt(i);
if (! (c === 32 || c === 10 || c === 9)) { break; }
i++;
}
chunks[j] = chunks[j].slice(length + (i - mem));
current = i;
if (chunks[j].length === 0 && j < chunks.length - 1) { j++; }
if (typeof(match) === 'string') {
return match;
} else {
return match.length === 1 ? match[0] : match;
}
}
}
// Same as $(), but don't change the state of the parser,
// just return the match.
function peek(tok) {
if (typeof(tok) === 'string') {
return input.charAt(i) === tok;
} else {
return !!tok.test(chunks[j]);
}
}
this.env = env = env || {};
this.env.filename = this.env.filename || null;
this.env.inputs = this.env.inputs || {};
// The Parser
parser = {
// Parse an input string into an abstract syntax tree.
// Throws an error on parse errors.
parse: function(str) {
var root, error = null;
i = j = current = 0;
chunks = [];
input = str.replace(/\r\n/g, '\n');
if (env.filename) {
that.env.inputs[env.filename] = input;
}
// Split the input into chunks.
chunks = (function (chunks) {
var j = 0,
skip = /(?:@\{[\w-]+\}|[^"'`\{\}\/\(\)\\])+/g,
comment = /\/\*(?:[^*]|\*+[^\/*])*\*+\/|\/\/.*/g,
string = /"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'|`((?:[^`]|\\.)*)`/g,
level = 0,
match,
chunk = chunks[0],
inParam;
for (var i = 0, c, cc; i < input.length;) {
skip.lastIndex = i;
match = skip.exec(input);
if (match) {
if (match.index === i) {
i += match[0].length;
chunk.push(match[0]);
}
}
c = input.charAt(i);
comment.lastIndex = string.lastIndex = i;
match = string.exec(input)
if (match) {
if (match.index === i) {
i += match[0].length;
chunk.push(match[0]);
continue;
}
}
if (!inParam && c === '/') {
cc = input.charAt(i + 1);
if (cc === '/' || cc === '*') {
match = comment.exec(input);
if (match) {
if (match.index === i) {
i += match[0].length;
chunk.push(match[0]);
continue;
}
}
}
}
switch (c) {
case '{':
if (!inParam) {
level ++;
}
else {
inParam = false;
}
chunk.push(c);
break;
case '}':
if (!inParam) {
level --;
chunk.push(c);
chunks[++j] = chunk = [];
}
else {
inParam = false;
chunk.push(c);
}
break;
case '(':
if (!inParam) {
inParam = true;
}
else {
inParam = false;
}
chunk.push(c);
break;
case ')':
if (inParam) {
inParam = false;
}
chunk.push(c);
break;
default:
chunk.push(c);
}
i++;
}
if (level !== 0) {
error = {
index: i - 1,
message: (level > 0) ? "missing closing `}`" : "missing opening `{`",
filename: env.filename
};
}
return chunks.map(function (c) { return c.join(''); });
})([[]]);
if (error) {
util.error(env, error);
throw new Error('N/A');
}
// Start with the primary rule.
// The whole syntax tree is held under a Ruleset node,
// with the `root` property set to true, so no `{}` are
// output.
root = new tree.Ruleset([], $(this.parsers.primary));
root.root = true;
// Get an array of Ruleset objects, flattened
// and sorted according to specificitySort
root.toList = (function() {
return function(env) {
env.frames = env.frames || [];
// call populates Invalid-caused errors
var definitions = this.flatten([], [], env);
definitions.sort(specificitySort);
return definitions;
};
})();
// Sort rules by specificity: this function expects selectors to be
// split already.
//
// Written to be used as a .sort(Function);
// argument.
//
// [1, 0, 0, 467] > [0, 0, 1, 520]
var specificitySort = function(a, b) {
var as = a.specificity;
var bs = b.specificity;
if (as[0] != bs[0]) return bs[0] - as[0];
if (as[1] != bs[1]) return bs[1] - as[1];
if (as[2] != bs[2]) return bs[2] - as[2];
return bs[3] - as[3];
};
return root;
},
// Here in, the parsing rules/functions
//
// The basic structure of the syntax tree generated is as follows:
//
// Ruleset -> Rule -> Value -> Expression -> Entity
//
// In general, most rules will try to parse a token with the `$()` function, and if the return
// value is truly, will return a new node, of the relevant type. Sometimes, we need to check
// first, before parsing, that's when we use `peek()`.
parsers: {
// The `primary` rule is the *entry* and *exit* point of the parser.
// The rules here can appear at any level of the parse tree.
//
// The recursive nature of the grammar is an interplay between the `block`
// rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule,
// as represented by this simplified grammar:
//
// primary → (ruleset | rule)+
// ruleset → selector+ block
// block → '{' primary '}'
//
// Only at one point is the primary rule not called from the
// block rule: at the root level.
primary: function() {
var node, root = [];
while ((node = $(this.rule) || $(this.ruleset) ||
$(this.comment)) ||
$(/^[\s\n]+/) || (node = $(this.invalid))) {
if (node) root.push(node);
}
return root;
},
invalid: function () {
var chunk = $(/^[^;\n]*[;\n]/);
// To fail gracefully, match everything until a semicolon or linebreak.
if (chunk) {
return new tree.Invalid(chunk, memo, null, env.filename);
}
},
// We create a Comment node for CSS comments `/* */`,
// but keep the LeSS comments `//` silent, by just skipping
// over them.
comment: function() {
if (input.charAt(i) !== '/') return;
var comment = $(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/);
if (input.charAt(i + 1) === '/') {
return new tree.Comment($(/^\/\/.*/), true);
} else if (comment) {
return new tree.Comment(comment);
}
},
// Entities are tokens which can be found inside an Expression
entities: {
// A string, which supports escaping " and ' "milky way" 'he\'s the one!'
quoted: function() {
if (input.charAt(i) !== '"' && input.charAt(i) !== "'") return;
var str = $(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/);
if (str) {
return new tree.Quoted(str[1] || str[2]);
}
},
// A reference to a Mapnik field, like [NAME]
// Behind the scenes, this has the same representation, but Carto
// needs to be careful to warn when unsupported operations are used.
field: function() {
if (! $('[')) return;
var field_name = $(/(^[^\]]+)/);
if (! $(']')) return;
if (field_name) return new tree.Field(field_name[1]);
},
// This is a comparison operator
comparison: function() {
var str = $(/^=~|=|!=|<=|>=|<|>/);
if (str) {
return str;
}
},
// A catch-all word, such as: hard-light
// These can start with either a letter or a dash (-),
// and then contain numbers, underscores, and letters.
keyword: function() {
var k = $(/^[A-Za-z-]+[A-Za-z-0-9_]*/);
if (k) { return new tree.Keyword(k); }
},
// A function call like rgb(255, 0, 255)
// The arguments are parsed with the `entities.arguments` parser.
call: function() {
var name, args;
if (!(name = /^([\w\-]+|%)\(/.exec(chunks[j]))) return;
name = name[1];
if (name === 'url') {
// url() is handled by the url parser instead
return null;
} else {
i += name.length;
}
$('('); // Parse the '(' and consume whitespace.
args = $(this.entities['arguments']);
if (!$(')')) return;
if (name) {
return new tree.Call(name, args, env.filename, i);
}
},
// Arguments are comma-separated expressions
'arguments': function() {
var args = [], arg;
arg = $(this.expression);
while (arg) {
args.push(arg);
if (! $(',')) { break; }
arg = $(this.expression);
}
return args;
},
literal: function() {
return $(this.entities.dimension) ||
$(this.entities.keywordcolor) ||
$(this.entities.hexcolor) ||
$(this.entities.quoted);
},
// Parse url() tokens
//
// We use a specific rule for urls, because they don't really behave like
// standard function calls. The difference is that the argument doesn't have
// to be enclosed within a string, so it can't be parsed as an Expression.
url: function() {
var value;
if (input.charAt(i) !== 'u' || !$(/^url\(/)) return;
value = $(this.entities.quoted) || $(this.entities.variable) ||
$(/^[\-\w%@$\/.&=:;#+?~]+/) || '';
if (! $(')')) {
return new tree.Invalid(value, memo, 'Missing closing ) in URL.', env.filename);
} else {
return new tree.URL((typeof value.value !== 'undefined' ||
value instanceof tree.Variable) ?
value : new tree.Quoted(value));
}
},
// A Variable entity, such as `@fink`, in
//
// width: @fink + 2px
//
// We use a different parser for variable definitions,
// see `parsers.variable`.
variable: function() {
var name, index = i;
if (input.charAt(i) === '@' && (name = $(/^@[\w-]+/))) {
return new tree.Variable(name, index, env.filename);
}
},
hexcolor: function() {
var rgb;
if (input.charAt(i) === '#' && (rgb = $(/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})/))) {
var hsl = chroma(rgb[0]).hsl();
return new tree.Color(hsl, 1, false);
}
},
keywordcolor: function() {
var rgb = chunks[j].match(/^[a-z]+/);
if (rgb && rgb[0] in that.env.ref.data.colors) {
var data = that.env.ref.data.colors[$(/^[a-z]+/)];
var a = 1;
if (data.length > 3) {
a = data[3];
}
var hsl = chroma(data.slice(0, 3)).hsl();
return new tree.Color(hsl, a, false);
}
},
// A Dimension, that is, a number and a unit. The only
// unit that has an effect is %
dimension: function() {
var c = input.charCodeAt(i);
if ((c > 57 || c < 45) || c === 47) return;
var value = $(/^(-?\d*\.?\d+(?:[eE][-+]?\d+)?)(\%|\w+)?/);
if (value) {
return new tree.Dimension(value[1], value[2], memo, env.filename);
}
}
},
// The variable part of a variable definition.
// Used in the `rule` parser. Like @fink:
variable: function() {
var name;
if (input.charAt(i) === '@' && (name = $(/^(@[\w-]+)\s*:/))) {
return name[1];
}
},
// Entities are the smallest recognized token,
// and can be found inside a rule's value.
entity: function() {
return $(this.entities.call) ||
$(this.entities.literal) ||
$(this.entities.field) ||
$(this.entities.variable) ||
$(this.entities.url) ||
$(this.entities.keyword);
},
// A Rule terminator. Note that we use `peek()` to check for '}',
// because the `block` rule will be expecting it, but we still need to make sure
// it's there, if ';' was ommitted.
end: function() {
return $(';') || peek('}');
},
// Elements are the building blocks for Selectors. They consist of
// an element name, such as a tag a class, or `*`.
element: function() {
var e = $(/^(?:[.#][\w\-]+|\*|Map)/);
if (e) return new tree.Element(e);
},
// Attachments allow adding multiple lines, polygons etc. to an
// object. There can only be one attachment per selector.
attachment: function() {
var s = $(/^::([\w\-]+(?:\/[\w\-]+)*)/);
if (s) return s[1];
},
// Selectors are made out of one or more Elements, see above.
selector: function() {
var a, attachment,
e, elements = [],
f, filters = new tree.Filterset(),
z, zooms = [],
segments = 0, conditions = 0;
while (
(e = $(this.element)) ||
(z = $(this.zoom)) ||
(f = $(this.filter)) ||
(a = $(this.attachment))
) {
segments++;
if (e) {
elements.push(e);
} else if (z) {
zooms.push(z);
conditions++;
} else if (f) {
var err = filters.add(f);
if (err) {
util.error(env, {
message: err,
index: i - 1,
filename: env.filename
});
throw new Error('N/A');
}
conditions++;
} else if (attachment) {
util.error(env, {
message: 'Encountered second attachment name.\n',
index: i - 1,
filename: env.filename
});
throw new Error('N/A');
} else {
attachment = a;
}
var c = input.charAt(i);
if (c === '{' || c === '}' || c === ';' || c === ',') { break; }
}
if (segments) {
return new tree.Selector(filters, zooms, elements, attachment, conditions, memo);
}
},
filter: function() {
save();
var key, op, val;
if (! $('[')) return;
if ((key = $(/^[a-zA-Z0-9\-_]+/) ||
$(this.entities.quoted) ||
$(this.expression) ||
$(this.entities.variable) ||
$(this.entities.keyword) ||
$(this.entities.field))) {
// TODO: remove at 1.0.0
if (key instanceof tree.Quoted) {
key = new tree.Field(key.toString());
}
if ((op = $(this.entities.comparison)) &&
(val = $(this.expression) ||
$(this.entities.quoted) ||
$(this.entities.variable) ||
$(this.entities.dimension) ||
$(this.entities.keyword) ||
$(this.entities.field))) {
if (! $(']')) {
util.error(env, {
message: 'Missing closing ] of filter.',
index: memo - 1,
filename: env.filename
});
throw new Error('N/A');
}
if (!key.is) key = new tree.Field(key);
return new tree.Filter(key, op, val, memo, env.filename);
}
}
},
zoom: function() {
save();
var op, val;
if ($(/^\[\s*zoom/g) &&
(op = $(this.entities.comparison)) &&
(val = $(this.entities.variable) || $(this.entities.dimension)) && $(']')) {
return new tree.Zoom(op, val, memo, env.filename);
} else {
// backtrack
restore();
}
},
// The `block` rule is used by `ruleset`
// It's a wrapper around the `primary` rule, with added `{}`.
block: function() {
var content;
if ($('{') && (content = $(this.primary)) && $('}')) {
return content;
}
},
// div, .class, body > p {...}
ruleset: function() {
var selectors = [], s, rules;
save();
s = $(this.selector);
while (s) {
selectors.push(s);
while ($(this.comment)) {
// do nothing
}
if (! $(',')) { break; }
while ($(this.comment)) {
// do nothing
}
s = $(this.selector);
}
if (s) {
while ($(this.comment)) {
// do nothing
}
}
if (selectors.length > 0 && (rules = $(this.block))) {
if (selectors.length === 1 &&
selectors[0].elements.length &&
selectors[0].elements[0].value === 'Map') {
var rs = new tree.Ruleset(selectors, rules);
rs.isMap = true;
return rs;
}
return new tree.Ruleset(selectors, rules);
} else {
// Backtrack
restore();
}
},
rule: function() {
var name, value, c = input.charAt(i);
save();
if (c === '.' || c === '#') { return; }
if ((name = $(this.variable) || $(this.property))) {
value = $(this.value);
if (value && $(this.end)) {
return new tree.Rule(env.ref, name, value, memo, env.filename);
} else {
restore();
}
}
},
font: function() {
var value = [], expression = [], e;
e = $(this.entity);
while (e) {
expression.push(e);
e = $(this.entity);
}
value.push(new tree.Expression(expression));
if ($(',')) {
e = $(this.expression);
while (e) {
value.push(e);
if (! $(',')) { break; }
e = $(this.expression);
}
}
return new tree.Value(value);
},
// A Value is a comma-delimited list of Expressions
// In a Rule, a Value represents everything after the `:`,
// and before the `;`.
value: function() {
var e, expressions = [];
e = $(this.expression);
while (e) {
expressions.push(e);
if (! $(',')) { break; }
e = $(this.expression);
}
if (expressions.length > 1) {
return new tree.Value(expressions.map(function(e) {
return e.value[0];
}));
} else if (expressions.length === 1) {
return new tree.Value(expressions);
}
},
// A sub-expression, contained by parenthensis
sub: function() {
var e;
if ($('(') && (e = $(this.expression)) && $(')')) {
return e;
}
},
// This is a misnomer because it actually handles multiplication
// and division.
multiplication: function() {
var m, a, op, operation;
m = $(this.operand);
if (m) {
while ((op = ($('/') || $('*') || $('%'))) && (a = $(this.operand))) {
operation = new tree.Operation(op, [operation || m, a], memo, env.filename);
}
return operation || m;
}
},
addition: function() {
var m, a, op, operation;
m = $(this.multiplication);
if (m) {
while ((op = $(/^[-+]\s+/) || (input.charAt(i - 1) != ' ' && ($('+') || $('-')))) &&
(a = $(this.multiplication))) {
operation = new tree.Operation(op, [operation || m, a], memo, env.filename);
}
return operation || m;
}
},
// An operand is anything that can be part of an operation,
// such as a Color, or a Variable
operand: function() {
return $(this.sub) || $(this.entity);
},
// Expressions either represent mathematical operations,
// or white-space delimited Entities. @var * 2
expression: function() {
var e, entities = [];
e = $(this.addition);
while (e || $(this.entity)) {
entities.push(e);
e = $(this.addition);
}
if (entities.length > 0) {
return new tree.Expression(entities);
}
},
property: function() {
var name = $(/^(([a-z][-a-z_0-9]*\/)?\*?-?[-a-z_0-9]+)\s*:/);
if (name) return name[1];
}
}
};
return parser;
};