UNPKG

carto

Version:

Mapnik Stylesheet Compiler

784 lines (691 loc) 29.3 kB
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; };