From abd3d940eae065bce6ef61f0fb261a5756658ce9 Mon Sep 17 00:00:00 2001 From: nebulon42 Date: Sun, 9 Jul 2017 22:49:33 +0200 Subject: [PATCH] add the ability to use custom references for validating rules, ref #413, document JS API, fixes #479, update man page --- CHANGELOG.md | 9 ++ README.md | 5 +- bin/carto | 16 +-- docs/installation_usage.rst | 182 +++++++++++++++++++++++++++++++++-- docs/mml.rst | 2 + lib/carto/parser.js | 6 +- lib/carto/renderer.js | 84 +++++++++++++--- lib/carto/tree/call.js | 6 +- lib/carto/tree/definition.js | 4 +- lib/carto/tree/filter.js | 6 +- lib/carto/tree/layer.js | 5 +- lib/carto/tree/reference.js | 161 ++++++++++++++++++++----------- lib/carto/tree/rule.js | 36 +++---- man/carto.1 | 29 ++++-- test/bincarto.test.js | 4 +- test/filter.test.js | 8 +- test/reference.test.js | 58 +++++++++++ test/rendering.test.js | 8 +- test/specificity.test.js | 5 +- 19 files changed, 487 insertions(+), 147 deletions(-) create mode 100644 test/reference.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e3fe0c77..cdc259aa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ for Mapnik XML and `json` for the JSON variant (part of [#413](https://github.co For Mapnik XML all character data as tag content is now prefixed with CDATA. * Expressions are allowed in filters e.g. `[height] % 50 = 0` or `[height] + 10 = 0`, fields have to be properly written within brackets ([#377](https://github.com/mapbox/carto/issues/377)) +* carto now accepts custom references for validating rules (part of [#413](https://github.com/mapbox/carto/issues/413)) +* The JavaScript API has been documented ([#479](https://github.com/mapbox/carto/issues/479)) ### Breaking changes @@ -33,6 +35,13 @@ string. `data` contains the output as before and `msg` now contains an array of In case of errors `data` is `null`. * carto now only throws errors in case of program failures. All other style processing related errors can be found in the `msg` property (see above). +* The constructor of `carto.Renderer` now only takes one options object +instead of a `env` and `options` parameter. +* The option `mapnik_version` and `validation_data` of `carto.Renderer` have +been renamed to `version` and `validationData` respectively. +* `carto.Renderer` now checks each option before forwarding them to the rendering +chain. Supported options are defined in the documentation. If you use a option that is +missing please open a issue. ## 0.18.1 diff --git a/README.md b/README.md index 045c8f626..d885d0b0a 100644 --- a/README.md +++ b/README.md @@ -67,14 +67,13 @@ The `Renderer` interface is the main API for developers, and it takes an MML fil try { var data = fs.readFileSync(input, 'utf-8'); - var mml = new carto.MML(); + var mml = new carto.MML({}); mml.load(path.dirname(input), data, function (err, data) { var output = {}; if (!err) { output = new carto.Renderer({ - filename: input, - local_data_dir: path.dirname(input), + filename: input }).render(data); } diff --git a/bin/carto b/bin/carto index 4eb728fa2..a789781e8 100755 --- a/bin/carto +++ b/bin/carto @@ -3,7 +3,6 @@ var path = require('path'), fs = require('fs'), carto = require('../lib/carto'), - semver = require('semver'), url = require('url'), _ = require('lodash'), util = require('../lib/carto/util'); @@ -17,7 +16,7 @@ var yargs = require('yargs') .options('b', {alias:'benchmark', boolean:true, describe:'Outputs total compile time'}) .options('l', {alias:'localize', boolean:true, default:false, describe:'Use millstone to localize resources when loading an MML'}) .options('n', {alias:'nosymlink', boolean:true, describe:'Use absolute paths instead of symlinking files'}) - .options('a', {alias:'api', describe:'Specify Mapnik API version', default:carto.tree.Reference.getLatest()}) + .options('a', {alias:'api', describe:'Specify Mapnik API version'}) .options('f', {alias:'file', describe:'Outputs to the given file instead of stdout.'}) .options('o', {alias:'output', describe:'Specify output format (mapnik, json)', default:'mapnik'}) .options('q', {alias:'quiet', boolean:true, default:false, describe:'Do not output any warnings'}) @@ -45,13 +44,6 @@ if (!input) { process.exit(1); } -if (options.api) { - if (!semver.valid(options.api)) { - console.error("carto: invalid Mapnik API version. A valid version is e.g. 3.0.0 or 3.0.10"); - process.exit(1); - } -} - if (options.benchmark) { var start = +new Date; } @@ -96,10 +88,8 @@ function compile(err, data) { benchmark: options.benchmark, ppi: options.ppi, quiet: options.quiet, - outputFormat: options.output - }, - { - mapnik_version: options.api + outputFormat: options.output, + version: options.api }); try { var output; diff --git a/docs/installation_usage.rst b/docs/installation_usage.rst index 724a86e31..22c506ad6 100644 --- a/docs/installation_usage.rst +++ b/docs/installation_usage.rst @@ -22,8 +22,8 @@ Optionally you may also want to install millstone which is required for resolvin Having *millstone* installed specifically enables support for localizing external resources (URLs and local files) referenced in your MML file, and detecting projections (using `node-srs `_). -Usage -===== +Usage from the command line +=========================== Now that Carto is installed you should have a *carto* command line tool available that can be run on a CartoCSS project:: @@ -61,9 +61,13 @@ The following command line options are available: -v / --version Display version information +Usage from JavaScript (Carto API) +================================= -Alternatively, Carto can be used from JavaScript. The *Renderer* interface is the main API for developers, -and it takes an MML file as a string as input. :: +Alternatively, Carto can be used from JavaScript. While you theoretically could +instantiate almost all of the classes the main outward facing stable interfaces +are the ``Renderer`` and the ``MML`` interface. We start with an example and describe +the details afterwards:: // defined variables: // - input (the name or identifier of the file being parsed) @@ -71,14 +75,13 @@ and it takes an MML file as a string as input. :: try { var data = fs.readFileSync(input, 'utf-8'); - var mml = new carto.MML(); + var mml = new carto.MML({}); mml.load(path.dirname(input), data, function (err, data) { var output = {}; if (!err) { output = new carto.Renderer({ - filename: input, - local_data_dir: path.dirname(input), + filename: input }).render(data); } @@ -103,5 +106,170 @@ and it takes an MML file as a string as input. :: ... } +MML +--- + +The MML interface loads and processes a MML file (see :ref:`mml-file-structure` for details). +You instantiate the class with ``carto.MML``. The constructor takes a options object with +the following possible attributes: + +* ``localize`` *boolean* (same as -l / --localize on the command line) - this uses + `millstone `_ to localize stylesheet resources +* ``nosymlink`` *boolean* (same as -n / --nosymlink on the command line) - for millstone, tells + it to use unmodified paths instead of symlinking files + +By calling ``load(basedir, data, callback)`` the MML file is loaded and processed. +This method does not perform reading from a file, so you have to read the contents +of the file yourself and provide it as string to the method via the ``data`` parameter to the +load function. The ``basedir`` parameter is used to resolve stylesheet references. +When the processing is finished the specified ``callback`` function is called, which +has the following signature:: + + function (err, data) {} + +If an error occurred you find the message within ``err`` and ``data`` is ``null``. +When successful you find the processed MML data structure in ``data`` and ``err`` is ``null``. +The structure within ``data`` is excpected by the ``Renderer`` interface's ``render`` method. + .. note:: If you want to use Carto within the browser you should not use MML loading via ``carto.MML.load``. Instead you should supply the JSON of the MML including the Stylesheet strings directly to ``carto.Renderer.render``. + +Renderer +-------- + +The Renderer interface performs the parsing and transformation for rendering from +a MML file string (either self loaded or loaded through the MML interface) or from a MSS +file string (without layers). You instantiate the class with ``carto.Renderer``. +The constructor takes a options object with the following possible attributes: + +* ``benchmark`` *boolean* (similar to -b / --benchmark on the command line) - specifies + if carto should run in benchmarking mode +* ``effects`` *array* - a container for side-effects limited to FontSets +* ``filename`` *string* - name of the input file, used to format errors and warnings +* ``outputFormat`` *string [mapnik|json]* (similar to -o / --output on the command line) + - specifies which format the output should have, either Mapnik XML or JSON similar to Mapnik XML +* ``ppi`` *float* (similar to -ppi on the command line) - Pixels per inch used to convert m, mm, cm, in, pt, pc to pixels +* ``quiet`` *boolean* (similar to -q / --quiet on the command line) - if carto should output + warnings or not +* ``reference`` *class* - carto uses a reference to validate input. You can specify your own + which has to adhere to the specification. (see :ref:`reference`) +* ``validationData`` *object* + + * ``fonts`` *array* - a list of fonts that carto should use to validate if used fonts are valid/present + +* ``version`` *string (semver)* (similar to -a / --api on the command line) - specify which + Mapnik API version carto should use + +``carto.Renderer`` offers two methods for actual rendering. You can either use ``render(data)`` or +``renderMSS(data)``. Both accept a string of either a processed MML file or a MSS style fragment. +The ``render`` method produces a full-featured style output while the ``renderMSS`` outputs only +a style fragment. Both return the following object:: + + { + msg: [], + data: '' + } + +If errors or warnings occurred during rendering you will find them in ``msg`` and ``data`` +will be ``null`` (in case of errors). The actual output is found in ``data`` if no errors +occurred. + +Util +---- + +Carto provides a Util class to assist you with e.g. message formatting. Like in the +example you can call ``getMessageToPrint`` with a received message object to output it +nicely formatted as string. + +.. _reference: + +Using a custom reference +------------------------ + +Carto uses a reference to validate input. This reference specifies which rules and functions +are valid and which types a rule can take. It also describes how rules are transformed for +the output. By default carto uses `mapnik-reference `_ +as reference, but you can also use your own. It has to adhere to the following specification:: + + { + versions: [], // array of versions (semver) as strings + latest: '', // latest version (semver) as strings + load: function (version) {} // return data structure for specified version + } + +The data structure returned by ``load`` has to look like this:: + + { + version: '', // version (semver) as string + style: {}, // rules that apply to the style as a whole + layer: {}, // rules that apply to a layer as a whole + symbolizers: {}, // rules that apply to different elements of the renderer, this elements make up the map + colors: {}, // color names and their mapping to RGB values + datasources: {} // possible data sources for the rendering library and their parameters + } + +.. note:: ``datasources`` is not yet used by carto for validation. + +All entries that contain rules are objects where there attributes are named after +a color, symbolizer or rule. ``style`` and ``layer`` have the same inner structure. +Here is an example:: + + { + 'filter-mode': { + type: {}, + doc: '', + 'default-value': '', + 'default-meaning': '' + } + ... + } + +``symbolizer`` first contains the possible symbolizers and then their rules:: + + { + polygon: { + fill: {}, + 'fill-opacity': {} + ... + } + ... + } + +``colors`` maps color names to their RGB values:: + + { + aliceblue: [ 240, 248, 255 ], + antiquewhite: [ 250, 235, 215 ] + ... + } + +``datasources`` is similar to ``symbolizers`` and contains first the possible data sources +and then their possible parameters:: + + { + csv: { + file: {}, + base: {} + ... + } + } + +Rules (all the parts that where specified with ``{}`` with a little preview at ``filter-mode``) +can have several attributes that are evaluated:: + + name: { + css: '', // rule name which is used in CartoCSS + default-meaning: '', // meaning of the default value + default-value: '', // default value of the rule + doc: '', // documentation about the rule + expression: bool, // whether this rule is a expression or not + functions: [], // array of arrays that contain function name and # of params e.g. ["matrix", 6] + range: '', // range of values that are allowed e.g. 0-1 + required: bool, // if this rule is required + status: '[unstable|experimental|deprecated]', // if omitted it means stable + type: '[bbox|boolean|color|float|functions|numbers|string|uri]', // type can also be an array of keywords + } + +.. caution:: Adherence to the specification is not assessed in-depth because that would + be too resource intensive. If you don't adhere to the specification it is quite likely + that you hit runtime errors. diff --git a/docs/mml.rst b/docs/mml.rst index 395e2961e..2a606dd9f 100644 --- a/docs/mml.rst +++ b/docs/mml.rst @@ -1,3 +1,5 @@ +.. _mml-file-structure: + MML File Structure ================== diff --git a/lib/carto/parser.js b/lib/carto/parser.js index 6efff1661..a77c6cf60 100644 --- a/lib/carto/parser.js +++ b/lib/carto/parser.js @@ -447,8 +447,8 @@ carto.Parser = function Parser(env) { keywordcolor: function() { var rgb = chunks[j].match(/^[a-z]+/); - if (rgb && rgb[0] in tree.Reference.data.colors) { - var data = tree.Reference.data.colors[$(/^[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]; @@ -670,7 +670,7 @@ carto.Parser = function Parser(env) { value = $(this.value); if (value && $(this.end)) { - return new tree.Rule(name, value, memo, env.filename); + return new tree.Rule(env.ref, name, value, memo, env.filename); } else { restore(); } diff --git a/lib/carto/renderer.js b/lib/carto/renderer.js index 3473d6bad..1e67483fa 100644 --- a/lib/carto/renderer.js +++ b/lib/carto/renderer.js @@ -2,10 +2,46 @@ var _ = require('lodash'), carto = require('./index'), util = require('./util'); -carto.Renderer = function Renderer(env, options) { - this.env = env || {}; +carto.Renderer = function Renderer(options) { + this.env = {}; this.options = options || {}; - this.options.mapnik_version = this.options.mapnik_version || carto.tree.Reference.getLatest(); + + // load different reference if given + if (_.has(this.options, 'reference') && !_.isNil(this.options.reference)) { + this.ref = new carto.tree.Reference(this.options.reference); + } + else { + this.ref = new carto.tree.Reference(); + } + + this.options.version = this.options.version || this.ref.getLatest(); + this.options.outputFormat = this.options.outputFormat || 'mapnik'; + + this.env.ref = this.ref; + if (_.has(this.options, 'quiet') && _.isBoolean(this.options.quiet)) { + this.env.quiet = this.options.quiet; + _.unset(this.options.quiet); + } + if (_.has(this.options, 'benchmark') && _.isBoolean(this.options.benchmark)) { + this.env.benchmark = this.options.benchmark; + _.unset(this.options.benchmark); + } + if (_.has(this.options, 'validationData')) { + this.env.validation_data = this.options.validationData; + _.unset(this.options.validationData); + } + if (_.has(this.options, 'ppi')) { + this.env.ppi = this.options.ppi; + _.unset(this.options.ppi); + } + if (_.has(this.options, 'effects')) { + this.env.effects = this.options.effects; + _.unset(this.options.effects); + } + if (_.has(this.options, 'filename')) { + this.env.filename = this.options.filename; + _.unset(this.options.filename); + } }; /** @@ -22,11 +58,10 @@ carto.Renderer.prototype.renderMSS = function render(data) { validation_data: false, effects: [], quiet: false, - outputFormat: 'mapnik' }).value(); try { - carto.tree.Reference.setVersion(this.options.mapnik_version); + this.ref.setVersion(this.options.version); } catch (err) { util.error(env, { @@ -38,6 +73,17 @@ carto.Renderer.prototype.renderMSS = function render(data) { }; } + // check if given reference is compliant with expectations + if (!this.ref.compliant) { + util.error(env, { + message: 'Could not use the given reference, because it does not adhere to the specification. See the documentation for details.' + }); + return { + msg: env.msg, + data: null + }; + } + var output = []; var styles = []; @@ -73,7 +119,7 @@ carto.Renderer.prototype.renderMSS = function render(data) { if (env.benchmark) console.timeEnd('Total Style generation'); if (!util.hasErrors(env.msg)) { - switch (env.outputFormat) { + switch (this.options.outputFormat) { case 'json': output = JSON.stringify(output, null, 2); break; @@ -119,12 +165,11 @@ carto.Renderer.prototype.render = function render(m) { validation_data: false, effects: [], ppi: 90.714, - quiet: false, - outputFormat: 'mapnik' + quiet: false }).value(); try { - carto.tree.Reference.setVersion(this.options.mapnik_version); + this.ref.setVersion(this.options.version); } catch (err) { util.error(env, { @@ -136,6 +181,17 @@ carto.Renderer.prototype.render = function render(m) { }; } + // check if given reference is compliant with expectations + if (!this.ref.compliant) { + util.error(env, { + message: 'Could not use the given reference, because it does not adhere to the specification. See the documentation for details.' + }); + return { + msg: env.msg, + data: null + }; + } + var output = []; var definitions = []; @@ -265,7 +321,7 @@ carto.Renderer.prototype.render = function render(m) { l.properties = props; } - output.push(carto.tree.LayerObject(l, styles)); + output.push(carto.tree.LayerObject(l, styles, this.ref.data['version'])); } if (env.effects.length) { @@ -274,7 +330,7 @@ carto.Renderer.prototype.render = function render(m) { }), output); } - var map_properties = getMapProperties(m, definitions, env); + var map_properties = getMapProperties(m, definitions, this.ref.data.symbolizers.map, env); if (!util.hasErrors(env.msg)) { @@ -374,7 +430,7 @@ carto.Renderer.prototype.render = function render(m) { '_content': output }; - switch (env.outputFormat) { + switch (this.options.outputFormat) { case 'json': output = JSON.stringify(output, null, 2); break; @@ -572,12 +628,12 @@ function foldStyle(style) { * * @param {Object} m the mml object. * @param {Array} definitions the output of toList. + * @param {Array} symbolizers map symbolizers * @param {Object} env * @return {String} rendered properties. */ -function getMapProperties(m, definitions, env) { +function getMapProperties(m, definitions, symbolizers, env) { var rules = {}; - var symbolizers = carto.tree.Reference.data.symbolizers.map; _(m).each(function(value, key) { if (key in symbolizers) rules[key] = value; diff --git a/lib/carto/tree/call.js b/lib/carto/tree/call.js index eb530168f..38c34fd3c 100644 --- a/lib/carto/tree/call.js +++ b/lib/carto/tree/call.js @@ -56,13 +56,13 @@ tree.Call.prototype = { }; } } else { - var fn = tree.Reference.mapnikFunctions[this.name]; + var fn = env.ref.functions[this.name]; if (fn === undefined) { - var functions = _.toPairs(tree.Reference.mapnikFunctions); + var functions = _.toPairs(env.ref.functions); // cheap closest, needs improvement. var name = this.name; var mean = functions.map(function(f) { - return [f[0], tree.Reference.editDistance(name, f[0]), f[1]]; + return [f[0], env.ref.editDistance(name, f[0]), f[1]]; }).sort(function(a, b) { return a[1] - b[1]; }); diff --git a/lib/carto/tree/definition.js b/lib/carto/tree/definition.js index ecc793750..cbc65f05b 100644 --- a/lib/carto/tree/definition.js +++ b/lib/carto/tree/definition.js @@ -126,7 +126,7 @@ tree.Definition.prototype.symbolizersToObject = function(env, symbolizers, zoom) if (symbolizer === '*') continue; sym_count++; - var fail = tree.Reference.requiredProperties(symbolizer, attributes); + var fail = env.ref.requiredProperties(symbolizer, attributes); if (fail) { var rule = attributes[Object.keys(attributes).shift()]; util.error(env, { @@ -150,7 +150,7 @@ tree.Definition.prototype.symbolizersToObject = function(env, symbolizers, zoom) index: attributes[j].index, filename: attributes[j].filename }); - var x = tree.Reference.selector(attributes[j].name); + var x = env.ref.selector(attributes[j].name); if (x && x.serialization && x.serialization === 'content') { selfclosing = false; tagcontent = attributes[j].ev(env).toObject(env, true); diff --git a/lib/carto/tree/filter.js b/lib/carto/tree/filter.js index 59dd2d562..3f944764d 100644 --- a/lib/carto/tree/filter.js +++ b/lib/carto/tree/filter.js @@ -30,15 +30,15 @@ tree.Filter.prototype.ev = function(env) { }; tree.Filter.prototype.toObject = function(env) { - if (tree.Reference.data.filter) { - if (this.key.is === 'keyword' && -1 === tree.Reference.data.filter.value.indexOf(this.key.toString())) { + if (env.ref.data.filter) { + if (this.key.is === 'keyword' && -1 === env.ref.data.filter.value.indexOf(this.key.toString())) { util.error(env, { message: this.key.toString() + ' is not a valid keyword in a filter expression', index: this.index, filename: this.filename }); } - if (this.val.is === 'keyword' && -1 === tree.Reference.data.filter.value.indexOf(this.val.toString())) { + if (this.val.is === 'keyword' && -1 === env.ref.data.filter.value.indexOf(this.val.toString())) { util.error(env, { message: this.val.toString() + ' is not a valid keyword in a filter expression', index: this.index, diff --git a/lib/carto/tree/layer.js b/lib/carto/tree/layer.js index 063d7bca6..52132c353 100644 --- a/lib/carto/tree/layer.js +++ b/lib/carto/tree/layer.js @@ -3,9 +3,8 @@ var semver = require('semver'), (function(tree) { -tree.LayerObject = function(obj, styles) { - var apiVersion = tree.Reference.data['version'], - dsOptions = [], +tree.LayerObject = function(obj, styles, apiVersion) { + var dsOptions = [], layerAttr = {}, layerContent = []; diff --git a/lib/carto/tree/reference.js b/lib/carto/tree/reference.js index 757cb5f89..ac43e3ad6 100644 --- a/lib/carto/tree/reference.js +++ b/lib/carto/tree/reference.js @@ -1,51 +1,101 @@ -// Carto pulls in a reference from the `mapnik-reference` -// module. This file builds indexes from that file for its various +// Carto needs a reference to validate input. +// The reference can be either `mapnik-reference` or customly set. +// This file builds indexes from that file for its various // options, and provides validation methods for property: value // combinations. (function(tree) { var _ = require('lodash'), - mapnik_reference = require('mapnik-reference'), - ref = {}; + semver = require('semver'); -ref.getLatest = function() { - return mapnik_reference.latest; +tree.Reference = function Reference(ref) { + this.data = null; + this.selector_cache = []; + this.functions = []; + this.required_cache = []; + + if (!_.isNil(ref)) { + if (this.checkCompliance(ref)) { + this.compliant = true; + this.ref = ref; + } + // fall back to Mapnik reference if non-compliant reference was given + else { + this.compliant = false; + this.ref = require('mapnik-reference'); + } + } + // use Mapnik reference if no reference given + else { + this.compliant = true; + this.ref = require('mapnik-reference'); + } }; -ref.setData = function(data) { - ref.data = data; - ref.selector_cache = generateSelectorCache(data); - ref.mapnikFunctions = generateMapnikFunctions(data); +tree.Reference.prototype.checkCompliance = function (ref) { + if (_.has(ref, 'latest') && _.has(ref, 'versions') && + _.isArray(ref.versions) && _.has(ref, 'load') && + _.isFunction(ref.load)) { + var data = ref.load(ref.latest); + if (_.has(data, 'version') && _.isString(data.version) && + _.has(data, 'style') && _.isObject(data.style) && + _.has(data, 'layer') && _.isObject(data.layer) && + _.has(data, 'symbolizers') && _.isObject(data.symbolizers) && + _.has(data, 'colors') && _.isObject(data.colors) && + _.has(data, 'datasources') && _.isObject(data.datasources)) { - ref.mapnikFunctions.matrix = [6]; - ref.mapnikFunctions.translate = [1, 2]; - ref.mapnikFunctions.scale = [1, 2]; - ref.mapnikFunctions.rotate = [1, 3]; - ref.mapnikFunctions.skewX = [1]; - ref.mapnikFunctions.skewY = [1]; + return true; + } + } + + return false; +}; - ref.required_cache = generateRequiredProperties(data); +tree.Reference.prototype.getLatest = function () { + return this.ref.latest; }; -ref.setVersion = function(version) { - try { - ref.setData(mapnik_reference.load(version)); +tree.Reference.prototype.setData = function (data) { + this.data = data; + this.selector_cache = generateSelectorCache(data); + this.functions = generateFunctions(data); + + this.functions.matrix = [6]; + this.functions.translate = [1, 2]; + this.functions.scale = [1, 2]; + this.functions.rotate = [1, 3]; + this.functions.skewX = [1]; + this.functions.skewY = [1]; + + this.required_cache = generateRequiredProperties(data); +}; + +tree.Reference.prototype.setVersion = function (version) { + if (semver.valid(version)) { + try { + this.setData(this.ref.load(version)); + } + catch (err) { + var e = new Error('Version ' + version + ' is not supported'); + e.stack = null; // do not show stack trace + throw e; + } } - catch (err) { - var e = new Error('Mapnik version ' + version + ' is not supported'); - e.stack = null; // do not show stack trace - throw e; + else { + var apiErr = new Error('Invalid API version. A valid version is e.g. 3.0.0 or 3.0.10'); + apiErr.stack = null; // do not show stack trace + throw apiErr; } }; -ref.selectorData = function(selector, i) { - if (ref.selector_cache[selector]) return ref.selector_cache[selector][i]; +tree.Reference.prototype.selectorData = function (selector, i) { + if (this.selector_cache[selector]) return this.selector_cache[selector][i]; }; -ref.validSelector = function(selector) { return !!ref.selector_cache[selector]; }; -ref.selectorName = function(selector) { return ref.selectorData(selector, 3); }; -ref.selector = function(selector) { return ref.selectorData(selector, 1); }; -ref.symbolizer = function(selector) { return ref.selectorData(selector, 2); }; +tree.Reference.prototype.validSelector = function (selector) { return !!this.selector_cache[selector]; }; +tree.Reference.prototype.selectorName = function (selector) { return this.selectorData(selector, 3); }; +tree.Reference.prototype.selector = function (selector) { return this.selectorData(selector, 1); }; +tree.Reference.prototype.symbolizer = function (selector) { return this.selectorData(selector, 2); }; function generateSelectorCache(data) { var index = {}; @@ -69,7 +119,7 @@ function generateSelectorCache(data) { return index; } -function generateMapnikFunctions(data) { +function generateFunctions(data) { var functions = {}; _.forEach(data.style, function (rule) { if (rule.type === 'functions') { @@ -110,8 +160,8 @@ function generateRequiredProperties(data) { return cache; } -ref.requiredProperties = function(symbolizer_name, rules) { - var req = ref.required_cache[symbolizer_name]; +tree.Reference.prototype.requiredProperties = function (symbolizer_name, rules) { + var req = this.required_cache[symbolizer_name]; for (var i in req) { if (!(req[i] in rules)) { return 'Property ' + req[i] + ' required for defining ' + @@ -121,7 +171,7 @@ ref.requiredProperties = function(symbolizer_name, rules) { }; // TODO: finish implementation - this is dead code -ref._validateValue = { +tree.Reference.prototype._validateValue = { 'font': function(env, value) { if (env.validation_data && env.validation_data.fonts) { return env.validation_data.fonts.indexOf(value) != -1; @@ -131,12 +181,12 @@ ref._validateValue = { } }; -ref.isFont = function(selector) { - return ref.selector(selector).validate == 'font'; +tree.Reference.prototype.isFont = function (selector) { + return this.selector(selector).validate == 'font'; }; // https://gist.github.com/982927 -ref.editDistance = function(a, b){ +tree.Reference.prototype.editDistance = function (a, b){ if (a.length === 0) return b.length; if (b.length === 0) return a.length; var matrix = []; @@ -156,7 +206,7 @@ ref.editDistance = function(a, b){ return matrix[b.length][a.length]; }; -function validateFunctions(value, selector) { +function validateFunctions(ref, value, selector) { if (value.value[0].is === 'string') return true; for (var i in value.value) { for (var j in value.value[i].value) { @@ -174,7 +224,7 @@ function validateFunctions(value, selector) { return true; } -function validateKeyword(value, selector) { +function validateKeyword(ref, value, selector) { if (typeof ref.selector(selector).type === 'object') { return ref.selector(selector).type .indexOf(value.value[0].value) !== -1; @@ -185,8 +235,8 @@ function validateKeyword(value, selector) { } } -ref.selectorStatus = function (selector) { - var selectorRef = ref.selector(selector); +tree.Reference.prototype.selectorStatus = function (selector) { + var selectorRef = this.selector(selector); if (_.has(selectorRef, 'status')) { return selectorRef.status; @@ -195,25 +245,25 @@ ref.selectorStatus = function (selector) { return 'stable'; }; -ref.validValue = function(env, selector, value) { +tree.Reference.prototype.validValue = function (env, selector, value) { var i; // TODO: handle in reusable way - if (!ref.selector(selector)) { + if (!this.selector(selector)) { return false; } else if (value.value[0].is == 'keyword') { - return validateKeyword(value, selector); + return validateKeyword(this, value, selector); } else if (value.value[0].is == 'undefined') { // caught earlier in the chain - ignore here so that // error is not overridden return true; - } else if (ref.selector(selector).type == 'numbers') { + } else if (this.selector(selector).type == 'numbers') { for (i in value.value) { if (value.value[i].is !== 'float') { return false; } } return true; - } else if (ref.selector(selector).type == 'tags') { + } else if (this.selector(selector).type == 'tags') { if (!value.value) return false; if (!value.value[0].value) { return value.value[0].is === 'tag'; @@ -222,34 +272,31 @@ ref.validValue = function(env, selector, value) { if (value.value[0].value[i].is !== 'tag') return false; } return true; - } else if (ref.selector(selector).type == 'functions') { + } else if (this.selector(selector).type == 'functions') { // For backwards compatibility, you can specify a string for `functions`-compatible // values, though they will not be validated. - return validateFunctions(value, selector); - } else if (ref.selector(selector).type === 'unsigned') { + return validateFunctions(this.ref, value, selector); + } else if (this.selector(selector).type === 'unsigned') { if (value.value[0].is === 'float') { value.value[0].round(); return true; } else { return false; } - } else if ((ref.selector(selector).expression)) { + } else if ((this.selector(selector).expression)) { return true; } else { - if (ref.selector(selector).validate) { + if (this.selector(selector).validate) { for (i = 0; i < value.value.length; i++) { - if (ref.selector(selector).type == value.value[i].is && - ref._validateValue[ref.selector(selector).validate](env, value.value[i].value)) { + if (this.selector(selector).type == value.value[i].is && + this._validateValue[this.selector(selector).validate](env, value.value[i].value)) { return true; } } return false; } else { - return ref.selector(selector).type == value.value[0].is; + return this.selector(selector).type == value.value[0].is; } } }; - -tree.Reference = ref; - })(require('../tree')); diff --git a/lib/carto/tree/rule.js b/lib/carto/tree/rule.js index 393b67e8b..ec9731f37 100644 --- a/lib/carto/tree/rule.js +++ b/lib/carto/tree/rule.js @@ -6,14 +6,14 @@ var util = require('../util'), // a rule is a single property and value combination, or variable // name and value combination, like // polygon-opacity: 1.0; or @opacity: 1.0; -tree.Rule = function Rule(name, value, index, filename) { +tree.Rule = function Rule(ref, name, value, index, filename) { var parts = name.split('/'); this.name = parts.pop(); this.instance = parts.length ? parts[0] : '__default__'; this.value = (value instanceof tree.Value) ? value : new tree.Value([value]); this.index = index; - this.symbolizer = tree.Reference.symbolizer(this.name); + this.symbolizer = ref.symbolizer(this.name); this.filename = filename; this.variable = (name.charAt(0) === '@'); }; @@ -43,8 +43,8 @@ tree.Rule.prototype.toString = function() { tree.Rule.prototype.validate = function (env) { var valid = true; - if (!tree.Reference.validSelector(this.name)) { - var mean = getMean(this.name); + if (!env.ref.validSelector(this.name)) { + var mean = getMean(this.name, env.ref); var mean_message = ''; if (mean[0][1] < 3) { mean_message = '. Did you mean ' + mean[0][0] + '?'; @@ -57,7 +57,7 @@ tree.Rule.prototype.validate = function (env) { }); } else { - var selectorStatus = tree.Reference.selectorStatus(this.name); + var selectorStatus = env.ref.selectorStatus(this.name); if (selectorStatus !== 'stable') { if (selectorStatus === 'deprecated') { util.warning(env, { @@ -83,8 +83,8 @@ tree.Rule.prototype.validate = function (env) { } if ((this.value instanceof tree.Value) && - !tree.Reference.validValue(env, this.name, this.value)) { - if (!tree.Reference.selector(this.name)) { + !env.ref.validValue(env, this.name, this.value)) { + if (!env.ref.selector(this.name)) { valid = false; util.error(env, { message: 'Unrecognized property: ' + @@ -94,12 +94,12 @@ tree.Rule.prototype.validate = function (env) { }); } else { var typename; - if (tree.Reference.selector(this.name).validate) { - typename = tree.Reference.selector(this.name).validate; - } else if (typeof tree.Reference.selector(this.name).type === 'object') { - typename = 'keyword (options: ' + tree.Reference.selector(this.name).type.join(', ') + ')'; + if (env.ref.selector(this.name).validate) { + typename = env.ref.selector(this.name).validate; + } else if (typeof env.ref.selector(this.name).type === 'object') { + typename = 'keyword (options: ' + env.ref.selector(this.name).type.join(', ') + ')'; } else { - typename = tree.Reference.selector(this.name).type; + typename = env.ref.selector(this.name).type; } if (typename !== 'font' || @@ -133,9 +133,9 @@ tree.Rule.prototype.validate = function (env) { return valid; } -function getMean(name) { - return Object.keys(tree.Reference.selector_cache).map(function(f) { - return [f, tree.Reference.editDistance(name, f)]; +function getMean(name, ref) { + return Object.keys(ref.selector_cache).map(function(f) { + return [f, ref.editDistance(name, f)]; }).sort(function(a, b) { return a[1] - b[1]; }); } @@ -147,13 +147,13 @@ tree.Rule.prototype.toObject = function(env, content, sep) { if (this.validate(env)) { if (this.variable) { return {}; - } else if (tree.Reference.isFont(this.name) && this.value.value.length > 1) { + } else if (env.ref.isFont(this.name) && this.value.value.length > 1) { var f = tree._getFontSet(env, this.value.value); return _.set({}, 'fontset-name', f.name); } else if (content) { return this.value.toString(env, this.name, sep); } else { - return _.set({}, tree.Reference.selectorName(this.name), this.value.toString(env, this.name)); + return _.set({}, env.ref.selectorName(this.name), this.value.toString(env, this.name)); } } @@ -162,7 +162,7 @@ tree.Rule.prototype.toObject = function(env, content, sep) { // TODO: Rule ev chain should add fontsets to env.frames tree.Rule.prototype.ev = function(context) { - return new tree.Rule(this.name, + return new tree.Rule(context.ref, this.name, this.value.ev(context), this.index, this.filename); diff --git a/man/carto.1 b/man/carto.1 index 9ee2ffd79..8281fa9d3 100644 --- a/man/carto.1 +++ b/man/carto.1 @@ -1,14 +1,10 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH CARTO "1" "May 2017" "carto 0.18.0 (Carto map stylesheet compiler)" "User Commands" +.TH CARTO "1" "July 2017" "carto 0.18.0 (Carto map stylesheet compiler)" "User Commands" .SH NAME carto \- manual page for carto 0.18.0 (Carto map stylesheet compiler) .SH SYNOPSIS .B carto \fI\,\/\fR -.SH DESCRIPTION -Carto is a stylesheet renderer for Mapnik. It's an evolution of -the Cascadenik idea and language, with an emphasis on speed and -flexibility. .SH OPTIONS .TP \fB\-h\fR, \fB\-\-help\fR @@ -28,7 +24,13 @@ Use millstone to localize resources when loading an MML Use absolute paths instead of symlinking files [boolean] .TP \fB\-a\fR, \fB\-\-api\fR -Specify Mapnik API version [default: "3.0.6"] +Specify Mapnik API version +.TP +\fB\-f\fR, \fB\-\-file\fR +Outputs to the given file instead of stdout. +.TP +\fB\-o\fR, \fB\-\-output\fR +Specify output format (mapnik, json) [default: "mapnik"] .TP \fB\-q\fR, \fB\-\-quiet\fR Do not output any warnings [boolean] [default: false] @@ -36,6 +38,15 @@ Do not output any warnings [boolean] [default: false] \fB\-\-ppi\fR Pixels per inch used to convert m, mm, cm, in, pt, pc to pixels [default: 90.714] -.SH REPORTING BUGS -Please report bugs on the GitHub issue tracker: -<\fBhttps://github.com/mapbox/carto/issues\fR> +.SH "SEE ALSO" +The full documentation for +.B carto +is maintained as a Texinfo manual. If the +.B info +and +.B carto +programs are properly installed at your site, the command +.IP +.B info carto +.PP +should give you access to the complete manual. diff --git a/test/bincarto.test.js b/test/bincarto.test.js index 33f396331..59e9552e2 100644 --- a/test/bincarto.test.js +++ b/test/bincarto.test.js @@ -19,7 +19,7 @@ describe('bin/carto', function() { var api = '1.0.0'; exec(util.format('node %s -a %s %s', bin, api, file), function(err, stdout, stderr) { assert.equal(1, err.code); - assert.equal("Error: Mapnik version 1.0.0 is not supported\n", stderr); + assert.equal("Error: Version 1.0.0 is not supported\n", stderr); done(); }); }); @@ -28,7 +28,7 @@ describe('bin/carto', function() { var api = 'api'; exec(util.format('node %s -a %s %s', bin, api, file), function(err, stdout, stderr) { assert.equal(1, err.code); - assert.equal("carto: invalid Mapnik API version. A valid version is e.g. 3.0.0 or 3.0.10\n", stderr); + assert.equal("Error: Invalid API version. A valid version is e.g. 3.0.0 or 3.0.10\n", stderr); done(); }); }); diff --git a/test/filter.test.js b/test/filter.test.js index 60d6bd1ba..e223bc2e1 100644 --- a/test/filter.test.js +++ b/test/filter.test.js @@ -6,19 +6,21 @@ require('../lib/carto/tree/filter'); describe('Field', function() { describe('basic functionality', function() { + var ref = new tree.Reference(); + ref.setVersion(ref.getLatest()); it('should be constructed', function() { var f = new tree.Filter(new tree.Field('foo'), '=', new tree.Dimension(1)); assert.ok(f); }); it('can be evaluated', function() { var f = new tree.Filter(new tree.Field('foo'), '=', new tree.Dimension(1)); - f.ev({}); + f.ev({ ref: ref }); assert.ok(f); }); it('yields object', function() { var f = new tree.Filter(new tree.Field('foo'), '=', new tree.Dimension(1)); - f.ev({}); - assert.equal(f.toObject({}), '[foo] = 1'); + f.ev({ ref: ref }); + assert.equal(f.toObject({ ref: ref }), '[foo] = 1'); }); }); }); diff --git a/test/reference.test.js b/test/reference.test.js new file mode 100644 index 000000000..bc8b4edc8 --- /dev/null +++ b/test/reference.test.js @@ -0,0 +1,58 @@ +var assert = require('assert'); + +var carto = require('../lib/carto'); + +describe('Reference', function() { + it('should accept custom reference adhering to spec', function(done) { + var customRef = { + load: function (wanted) { + return { + version: wanted, + style: {}, + layer: {}, + symbolizers: { + bla: { + width: { + css: 'bla-width', + type: 'float' + } + } + }, + colors: {}, + datasources: {} + }; + }, + latest: '1.5.0', + versions: [ + '1.0.0', + '1.5.0' + ] + }; + var renderer = new carto.Renderer({ + reference: customRef + }), + output = renderer.renderMSS('#test { bla-width: 1; }'); + assert.equal(output.msg, null); + assert.equal(output.data, '\n'); + done(); + }); + + it('should reject custom reference not adhering to spec', function(done) { + var customRef = { + load: function (wanted) { + return wanted; + }, + latest: '1.5.0', + versions: [ + '1.0.0', + '1.5.0' + ] + }; + var renderer = new carto.Renderer({ + reference: customRef + }), + output = renderer.renderMSS('#test { marker-color: #fff; }'); + assert.equal(output.msg[0].message, 'Could not use the given reference, because it does not adhere to the specification. See the documentation for details.'); + done(); + }); +}); diff --git a/test/rendering.test.js b/test/rendering.test.js index e8b6915d0..66dd87a4e 100644 --- a/test/rendering.test.js +++ b/test/rendering.test.js @@ -54,13 +54,9 @@ helper.files('rendering', 'mml', function(file) { renderer = null; if (api) { - renderer = new carto.Renderer(env, { - mapnik_version: api - }); - } - else { - renderer = new carto.Renderer(env); + env.version = api } + renderer = new carto.Renderer(env); output = renderer.render(mml); } else { diff --git a/test/specificity.test.js b/test/specificity.test.js index cecb80ad6..18c7fff20 100644 --- a/test/specificity.test.js +++ b/test/specificity.test.js @@ -26,9 +26,12 @@ describe('Specificity', function() { helper.files('specificity', 'mss', function(file) { it('should handle spec correctly in ' + file, function(done) { helper.file(file, function(content) { + var ref = new carto.tree.Reference(); + ref.setVersion(ref.getLatest()); var tree = (new carto.Parser({ paths: [ path.dirname(file) ], - filename: file + filename: file, + ref: ref })).parse(content); var mss = tree.toList({});