var carto = require('carto'); var torque = require('torque.js'); var Utils = require('builder/helpers/utils'); var _ = require('underscore'); var ERROR_REGEX = /.*:(\d+):(\d+)\s*(.*)/; var CartoParser = function (cartocss) { this.parse_env = null; this.ruleset = null; if (cartocss) { this.parse(cartocss); } }; CartoParser.prototype = { // value due to torque RESERVED_VARIABLES: ['mapnik-geometry-type', 'points_density', 'points_count', 'src', 'value', 'agg_value', 'agg_value_density'], parse: function (cartocss) { this.parse_env = { validation_data: false, frames: [], errors: [], error: function (obj) { obj.line = carto.Parser().extractErrorLine(cartocss, obj.index); this.errors.push(obj); } }; var self = this; var ruleset = null; var defs = null; try { // set default reference carto.tree.Reference.setData(carto.default_reference.version.latest); ruleset = (new carto.Parser(this.parse_env)).parse(cartocss); } catch (e) { // add the style.mss string to match the response from the server this.parse_env.errors = this.parseError(['style\.mss' + e.message]); return; } if (ruleset) { var existing = {}; var mapDef; var symbolizers; var i; var j; var r; this.definitions = defs = ruleset.toList(this.parse_env); for (i in defs) { if (defs[i].elements.length > 0) { if (defs[i].elements[0].value === 'Map') { mapDef = defs.splice(i, 1)[0]; } } } symbolizers = torque.cartocss_reference.version.latest.layer; if (mapDef) { mapDef.rules.forEach(function (r) { var key = r.name; var type; var element; if (!(key in symbolizers)) { self.parse_env.error({ message: 'Rule ' + key + ' not allowed for Map.', index: r.index }); } else { type = symbolizers[r.name].type; element = r.value.value[0].value[0]; if (!self._checkValidType(element, type)) { self.parse_env.error({ message: 'Expected type ' + type + '.', index: r.index }); } } }); } defs = carto.inheritDefinitions(defs, this.parse_env); defs = carto.sortStyles(defs, this.parse_env); for (i in defs) { for (j in defs[i]) { r = defs[i][j]; if (r && r.toXML) { r.toXML(this.parse_env, existing); } } } // toList uses parse_env.errors.message to put messages if (this.parse_env.errors.message) { _(this.parse_env.errors.message.split('\n')).each(function (m) { self.parse_env.errors.push(m); }); } } this.ruleset = ruleset; return this; }, _checkValidType: function (e, type) { if (['number', 'float'].indexOf(type) > -1) { return typeof e.value === 'number'; } else if (type === 'string') { return e.value !== 'undefined' && typeof e.value === 'string'; } else if (type.constructor === Array) { return type.indexOf(e.value) > -1 || e.value === 'linear'; } else if (type === 'color') { return this._checkValidColor(e); } return true; }, _checkValidColor: function (e) { var expectedArguments = {rgb: 3, hsl: 3, rgba: 4, hsla: 4}; return typeof e.rgb !== 'undefined' || expectedArguments[e.name] === e.args; }, /** * gets an array of parse errors from windshaft * and returns an array of {line:1, error: 'string'] with user friendly * strings. Parses errors in format: * * 'style.mss:7:2 Invalid code: asdasdasda' * * it could also get already parsed objects so we return them directly without parsing them again */ parseError: function (errors) { var parsedErrors = _.compact(errors).map(function (error) { if (_.isObject(error)) return error; var matchedError = error.match(ERROR_REGEX); return matchedError ? { line: parseInt(matchedError[1], 10), message: matchedError[3] } : { line: null, message: error }; }); // sort by line parsedErrors.sort(function (a, b) { return a.line - b.line; }); parsedErrors = _.uniq(parsedErrors, true, function (a) { return a.line + a.message; }); return parsedErrors; }, /** * return the error list, empty if there were no errors */ errors: function () { return this.parse_env ? this.parse_env.errors : []; }, _colorsFromRule: function (rule) { function searchRecursiveByType (v, t) { var res = []; var i; var r; for (i in v) { if (v[i] instanceof t) { res.push(v[i]); } else if (typeof v[i] === 'object') { r = searchRecursiveByType(v[i], t); if (r.length) { res = res.concat(r); } } } return res; } return searchRecursiveByType(rule.ev(this.parse_env), carto.tree.Color); }, _varsFromRule: function (rule) { function searchRecursiveByType (v, t) { var res = []; var i; var r; for (i in v) { if (v[i] instanceof t) { res.push(v[i]); } else if (typeof v[i] === 'object') { r = searchRecursiveByType(v[i], t); if (r.length) { res = res.concat(r); } } } return res; } return searchRecursiveByType(rule, carto.tree.Field); }, /** * Extract information from the carto using the provided method. * */ _extract: function (method, extractVariables) { var self = this; var columns = []; var definitions; var def; var d; var r; var f; var k; var rule; var columnList; var filter; var filter_key; if (this.ruleset) { definitions = this.ruleset.toList(this.parse_env); for (d in definitions) { def = definitions[d]; if (def.filters) { // extract from rules for (r in def.rules) { rule = def.rules[r]; columnList = method(this, rule); columns = columns.concat(columnList); } if (extractVariables) { for (f in def.filters) { filter = def.filters[f]; for (k in filter) { filter_key = filter[k]; if (filter_key.key && filter_key.key.value) { columns.push(filter_key.key.value); } } } } } } return _.reject(_.uniq(columns), function (v) { return _.contains(self.RESERVED_VARIABLES, v); }); } }, /** * return a list of colors used in cartocss */ colorsUsed: function (opt) { // extraction method var method = function (self, rule) { var colors = self._colorsFromRule(rule); return _.map(colors, function (color) { return color.rgb; }); }; var colors = this._extract(method, false); if (opt && opt.mode === 'hex') { colors = _.map(colors, function (color) { return Utils.rgbToHex(color[0], color[1], color[2]); }); } return colors; }, colorsUsedForLegend: function (opt) { var rules = this.getDefaultRules(); if (rules['image-filters']) { var method = function (self, rule) { var imageFillRule = rules['image-filters']; var colors = self._colorsFromRule(imageFillRule); return _.map(colors, function (f) { return f.rgb; }); }; var colors = this._extract(method, true); if (opt && opt.mode === 'hex') { colors = _.map(colors, function (color) { return Utils.rgbToHex(color[0], color[1], color[2]); }); } } return _.map(colors, function (color) { return { color: color }; }); }, /** * return a list of variables used in cartocss */ variablesUsed: function () { // extraction method var method = function (self, rule) { return _.map(self._varsFromRule(rule), function (f) { return f.value; }); }; return this._extract(method, true); }, /** * returns the default layer */ getDefaultRules: function () { var rules = []; var i; var def; var rulesMap = {}; for (i = 0; i < this.definitions.length; ++i) { def = this.definitions[i]; // all zooms and default attachment so we don't get conditional variables if (def.zoom === 8388607 && _.size(def.filters.filters) === 0 && def.attachment === '__default__') { rules = rules.concat(def.rules); } } for (i in rules) { var rule = rules[i]; rulesMap[rule.name] = rule; } return rulesMap; }, getRuleByName: function (definition, ruleName) { if (!definition._rulesByName) { var rulesMap = definition._rulesByName = {}; for (var r in definition.rules) { var rule = definition.rules[r]; rulesMap[rule.name] = rule; } } return definition._rulesByName[ruleName]; } }; module.exports = CartoParser;