Initial commit
This commit is contained in:
355
lib/assets/javascripts/builder/helpers/parser-css.js
Executable file
355
lib/assets/javascripts/builder/helpers/parser-css.js
Executable file
@@ -0,0 +1,355 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user