Skip to content

Instantly share code, notes, and snippets.

@tmcw tmcw/-
Created Feb 26, 2014

Embed
What would you like to do?
commit 3147a89a48a587b8dbc4720beaad55b3a2540fad
Author: Tom MacWright <macwright@gmail.com>
Date: Mon Feb 7 11:22:14 2011 -0500
Rename mess to carto.
diff --git a/Makefile b/Makefile
index ab44b13..0f67af7 100644
--- a/Makefile
+++ b/Makefile
@@ -11,6 +11,6 @@ test:
endif
doc:
- docco lib/mess/*.js lib/mess/tree/*.js
+ docco lib/carto/*.js lib/carto/tree/*.js
.PHONY: test
diff --git a/README.md b/README.md
index f30ae9e..ad5597f 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# mess.js
+# carto
Is a stylesheet renderer for Mapnik. It's an evolution of the [Cascadenik](https://github.com/mapnik/Cascadenik) idea and language, with an emphasis on speed and flexibility.
@@ -6,7 +6,7 @@ Is a stylesheet renderer for Mapnik. It's an evolution of the [Cascadenik](https
Follow the directions to install [node-zipfile](https://github.com/springmeyer/node-zipfile) and then:
- npm install mess
+ npm install carto
_note: possibly broken on ubuntu_
@@ -16,7 +16,7 @@ _incompatibility_
* MML files are assumed to be JSON, not XML. The files are near-identical to the XML files accepted by Cascadenik, just translated into JSON.
* Like Cascadenik, you can also include remote stylesheets, by including their URLs as simple strings in the Stylesheet array.
-mess.js MML:
+carto.js MML:
{
"srs": "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs",
@@ -58,9 +58,9 @@ Cascadenik MML
## Attachments
_new_
-In CSS, a certain object can only have one instance of a property. A `<div>` has a specific border width and color, rules that match better than others (#id instead of .class) override previous definitions. `mess.js` acts the same way normally for the sake of familiarity and organization, but Mapnik itself is more powerful.
+In CSS, a certain object can only have one instance of a property. A `<div>` has a specific border width and color, rules that match better than others (#id instead of .class) override previous definitions. `carto.js` acts the same way normally for the sake of familiarity and organization, but Mapnik itself is more powerful.
-Layers in Mapnik can have multiple [borders](http://trac.mapnik.org/wiki/LineSymbolizer) and multiple copies of other attributes. This ability is useful in drawing line outlines, like in the case of road borders or 'glow' effects around coasts. `mess.js` makes this accessible by allowing attachments to styles:
+Layers in Mapnik can have multiple [borders](http://trac.mapnik.org/wiki/LineSymbolizer) and multiple copies of other attributes. This ability is useful in drawing line outlines, like in the case of road borders or 'glow' effects around coasts. `carto.js` makes this accessible by allowing attachments to styles:
#world {
line-color: #fff;
@@ -72,7 +72,7 @@ Layers in Mapnik can have multiple [borders](http://trac.mapnik.org/wiki/LineSym
line-width: 6;
}
-Attachments are optional: if you don't define them, mess.js does overriding of styles just like Cascadenik.
+Attachments are optional: if you don't define them, carto.js does overriding of styles just like Cascadenik.
This brings us to another _incompatibility_: `line-inline` and `line-outline` have been removed from the language, because attachments are capable of the same trick.
@@ -84,7 +84,7 @@ Instead of the name attribute of the [TextSymbolizer](http://trac.mapnik.org/wik
<table>
<tr>
<th>cascadenik</th>
- <th>mess.js</th>
+ <th>carto.js</th>
</tr>
<tr>
<td valign='top'>
@@ -106,19 +106,19 @@ Instead of the name attribute of the [TextSymbolizer](http://trac.mapnik.org/wik
## Mapnik2
_new_
-`mess.js` is only compatible with [Mapnik2](http://trac.mapnik.org/wiki/Mapnik2). Compatibility with Mapnik 0.7.x is not planned.
+`carto.js` is only compatible with [Mapnik2](http://trac.mapnik.org/wiki/Mapnik2). Compatibility with Mapnik 0.7.x is not planned.
## Rasters and Buildings
_new_
-Rasters are supported in mess.js - it knows how to download `.vrt`, `.tiff`, and soon other raster formats, and the properties of the [RasterSymbolizer](http://trac.mapnik.org/wiki/RasterSymbolizer) are exposed in the language.
+Rasters are supported in carto.js - it knows how to download `.vrt`, `.tiff`, and soon other raster formats, and the properties of the [RasterSymbolizer](http://trac.mapnik.org/wiki/RasterSymbolizer) are exposed in the language.
-The [BuildingSymbolizer](http://trac.mapnik.org/wiki/BuildingSymbolizer) is also supported in `mess.js`. The code stores symbolizer types and properties in a JSON file (in `tree/reference.js`), so new Mapnik features can be quickly implemented here.
+The [BuildingSymbolizer](http://trac.mapnik.org/wiki/BuildingSymbolizer) is also supported in `carto.js`. The code stores symbolizer types and properties in a JSON file (in `tree/reference.js`), so new Mapnik features can be quickly implemented here.
## Variables & Expressions
_new_
-`mess.js` inherits from its basis in [less.js](http://lesscss.org/) some new features in CSS. One can define variables in stylesheets, and use expressions to modify them.
+`carto.js` inherits from its basis in [less.js](http://lesscss.org/) some new features in CSS. One can define variables in stylesheets, and use expressions to modify them.
@mybackground: #2B4D2D;
@@ -134,7 +134,7 @@ _new_
## Nested Styles
_new_
-`mess.js` also inherits nesting of rules from less.js.
+`carto.js` also inherits nesting of rules from less.js.
/* Applies to all layers with .land class */
.land {
@@ -163,11 +163,11 @@ This can be a convenient way to group style changes by zoom level:
## FontSets
_new_
-By defining multiple fonts in a `text-face-name` definition, you create [FontSets](http://trac.mapnik.org/wiki/FontSet) in `mess.js`. These are useful for supporting multiple character sets and fallback fonts for distributed styles.
+By defining multiple fonts in a `text-face-name` definition, you create [FontSets](http://trac.mapnik.org/wiki/FontSet) in `carto.js`. These are useful for supporting multiple character sets and fallback fonts for distributed styles.
<table>
<tr>
- <th>mess</th><th>XML</th>
+ <th>carto</th><th>XML</th>
</tr>
<tr>
<td valign='top'>
@@ -200,26 +200,26 @@ By defining multiple fonts in a `text-face-name` definition, you create [FontSet
#### Using the binary
- messc map_file.json
+ cartoc map_file.json
#### Using the code
-Currently `mess.js` is designed to be invoked from [node.js](http://nodejs.org/).
+Currently `carto.js` is designed to be invoked from [node.js](http://nodejs.org/).
The `Renderer` interface is the main API for developers, and it takes an MML file as a string as input.
// defined variables:
// - input (the name or identifier of the file being parsed)
// - data (a string containing the MML or an object of MML)
- var mess = require('mess');
+ var carto = require('carto');
- new mess.Renderer({
+ new carto.Renderer({
filename: input,
local_data_dir: path.dirname(input),
}).render(data, function(err, output) {
if (err) {
if (Array.isArray(err)) {
err.forEach(function(e) {
- mess.writeError(e, options);
+ carto.writeError(e, options);
});
} else { throw err; }
} else {
@@ -229,7 +229,7 @@ The `Renderer` interface is the main API for developers, and it takes an MML fil
## Credits
-`mess.js` is based on [less.js](https://github.com/cloudhead/less.js), a CSS compiler written by Alexis Sellier.
+`carto.js` is based on [less.js](https://github.com/cloudhead/less.js), a CSS compiler written by Alexis Sellier.
It depends on:
diff --git a/bin/carto b/bin/carto
new file mode 100755
index 0000000..c6d0d35
--- /dev/null
+++ b/bin/carto
@@ -0,0 +1,101 @@
+#!/usr/bin/env node
+
+var path = require('path'),
+ fs = require('fs'),
+ sys = require('sys');
+
+require.paths.unshift(path.join(__dirname, '../lib'), path.join(__dirname, '../lib/node'));
+
+var mess = require('carto');
+var args = process.argv.slice(1);
+var options = {
+ silent: false,
+ json: false
+};
+
+args = args.filter(function (arg) {
+ var match;
+
+ if (match = arg.match(/^--?([a-z][0-9a-z-]*)$/i)) { arg = match[1] }
+ else { return arg }
+
+ switch (arg) {
+ case 'v':
+ case 'version':
+ sys.puts("messc " + mess.version.join('.') + " (MESS Compiler) [JavaScript]");
+ process.exit(0);
+ break;
+ case 'verbose':
+ options.verbose = true;
+ break;
+ case 'd':
+ case 'debug':
+ options.debug = true;
+ break;
+ case 's':
+ case 'silent':
+ options.silent = true;
+ break;
+ case 'b':
+ case 'benchmark':
+ options.benchmark = true;
+ break;
+ case 'h':
+ case 'help':
+ sys.puts("Usage: messc source");
+ sys.puts("Options:");
+ sys.puts(" -j\tParse JSON map manifest");
+ process.exit(0);
+ break;
+ }
+});
+
+var input = args[1];
+if (input && input[0] != '/') {
+ input = path.join(process.cwd(), input);
+}
+var output = args[2];
+if (output && output[0] != '/') {
+ output = path.join(process.cwd(), output);
+}
+
+if (!input) {
+ sys.puts("messc: no input files");
+ process.exit(1);
+}
+
+if (options.benchmark) {
+ var start = +new Date;
+}
+
+fs.readFile(input, 'utf-8', function (e, data) {
+ if (e) {
+ sys.puts("messc: " + e.message);
+ process.exit(1);
+ }
+
+ new mess.Renderer({
+ filename: input,
+ debug: options.debug,
+ local_data_dir: path.dirname(input),
+ }).render(data, function(err, output) {
+ if (err) {
+ if (Array.isArray(err)) {
+ err.forEach(function(e) {
+ mess.writeError(e, options);
+ });
+ } else {
+ throw err;
+ }
+
+ process.exit(1);
+ } else {
+ if (!options.benchmark) {
+ sys.puts(output);
+ } else {
+ var duration = (+new Date) - start;
+ console.log('Benchmark: ' + (duration) + 'ms');
+ }
+ }
+ });
+});
diff --git a/bin/cartox b/bin/cartox
new file mode 100755
index 0000000..261be84
--- /dev/null
+++ b/bin/cartox
@@ -0,0 +1,123 @@
+#!/usr/bin/env node
+
+var xml2js = require('xml2js'),
+ fs = require('fs'),
+ _ = require('underscore')._,
+ path = require('path'),
+ sys = require('sys');
+
+require.paths.unshift(path.join(__dirname, '../lib'), path.join(__dirname, '../lib/node'));
+
+var mess = require('carto'),
+ args = process.argv.slice(1);
+
+var options = {
+ silent: false,
+ json: false
+};
+
+args = args.filter(function (arg) {
+ var match;
+
+ if (match = arg.match(/^--?([a-z][0-9a-z-]*)$/i)) { arg = match[1] }
+ else { return arg }
+
+/*
+ switch (arg) {
+ case 'h':
+ case 'help':
+ sys.puts("Usage: messc source");
+ sys.puts("Options:");
+ sys.puts(" -j\tParse JSON map manifest");
+ process.exit(0);
+ break;
+ }
+ */
+});
+
+var input = args[1];
+if (input && input[0] != '/') {
+ input = path.join(process.cwd(), input);
+}
+var output = args[2];
+if (output && output[0] != '/') {
+ output = path.join(process.cwd(), output);
+}
+
+function upStyle(s) {
+ this.name = s['@'].name;
+ this.rules = [];
+}
+
+upStyle.prototype.toMSS = function() {
+ return '.' + this.name
+ + ' {\n'
+ + this.rules.join('\n');
+ + '}';
+}
+
+function upRule(xmlRule) {
+ this.filters = xmlRule.Filter ? this.upFilter(xmlRule.Filter) : [];
+ this.rules = this.upSymbolizers(xmlRule);
+}
+
+upRule.prototype.upFilter = function(xmlFilter) {
+ var xmlFilters = xmlFilter.match(/(\(.*?\))/g);
+ return _.flatten(xmlFilters.map(function(f) {
+ return f.replace(/\[(\w+)\]/, "$1").replace(/\)|\(/g, "");
+ }));
+};
+
+upRule.prototype.upSymbolizers = function(xmlRule) {
+ var css_rules = [];
+ var symnames = _.map(_.keys(mess.tree.Reference.data.symbolizers), function(symbolizer) {
+ return [symbolizer.charAt(0).toUpperCase() +
+ symbolizer.slice(1).replace(/\-./, function(str) {
+ return str[1].toUpperCase();
+ }) + 'Symbolizer', mess.tree.Reference.data.symbolizers[symbolizer]];
+ });
+ var symmap = _.reduce(symnames, function(memo, s) {
+ memo[s[0]] = s[1];
+ return memo;
+ }, {});
+ var cssmap = function(symbolizer, name) {
+ return symmap[symbolizer][name].css;
+ }
+ for (var i in xmlRule) {
+ if (i in symmap) {
+ for (var j in xmlRule[i]['@']) {
+ if (symmap[i][j].type == 'uri') {
+ css_rules.push(cssmap(i, j) + ': url("' + xmlRule[i]['@'][j] + '");');
+ } else {
+ css_rules.push(cssmap(i, j) + ': "' + xmlRule[i]['@'][j] + '";');
+ }
+ }
+ }
+ }
+ return css_rules;
+};
+
+
+upRule.prototype.toMSS = function() {
+ return ' ' + this.filters.map(function(f) {
+ return '[' + f + ']';
+ }).join('')
+ + ' {\n '
+ + this.rules.join('\n ')
+ + '\n }';
+}
+
+fs.readFile(input, 'utf-8', function (e, data) {
+ var styles = [];
+ var firstParser = new xml2js.Parser();
+ firstParser.addListener('end', function(mapnik_xml) {
+ mapnik_xml.Style.forEach(function(s) {
+ var newStyle = new upStyle(s);
+ s.Rule.forEach(function(r) {
+ newStyle.rules.push((new upRule(r)).toMSS());
+ });
+ styles.push(newStyle.toMSS());
+ });
+ console.log(styles.join('\n'));
+ }).parseString(data);
+});
diff --git a/bin/mess-up b/bin/mess-up
deleted file mode 100755
index 771ef14..0000000
--- a/bin/mess-up
+++ /dev/null
@@ -1,123 +0,0 @@
-#!/usr/bin/env node
-
-var xml2js = require('xml2js'),
- fs = require('fs'),
- _ = require('underscore')._,
- path = require('path'),
- sys = require('sys');
-
-require.paths.unshift(path.join(__dirname, '../lib'), path.join(__dirname, '../lib/node'));
-
-var mess = require('mess'),
- args = process.argv.slice(1);
-
-var options = {
- silent: false,
- json: false
-};
-
-args = args.filter(function (arg) {
- var match;
-
- if (match = arg.match(/^--?([a-z][0-9a-z-]*)$/i)) { arg = match[1] }
- else { return arg }
-
-/*
- switch (arg) {
- case 'h':
- case 'help':
- sys.puts("Usage: messc source");
- sys.puts("Options:");
- sys.puts(" -j\tParse JSON map manifest");
- process.exit(0);
- break;
- }
- */
-});
-
-var input = args[1];
-if (input && input[0] != '/') {
- input = path.join(process.cwd(), input);
-}
-var output = args[2];
-if (output && output[0] != '/') {
- output = path.join(process.cwd(), output);
-}
-
-function upStyle(s) {
- this.name = s['@'].name;
- this.rules = [];
-}
-
-upStyle.prototype.toMSS = function() {
- return '.' + this.name
- + ' {\n'
- + this.rules.join('\n');
- + '}';
-}
-
-function upRule(xmlRule) {
- this.filters = xmlRule.Filter ? this.upFilter(xmlRule.Filter) : [];
- this.rules = this.upSymbolizers(xmlRule);
-}
-
-upRule.prototype.upFilter = function(xmlFilter) {
- var xmlFilters = xmlFilter.match(/(\(.*?\))/g);
- return _.flatten(xmlFilters.map(function(f) {
- return f.replace(/\[(\w+)\]/, "$1").replace(/\)|\(/g, "");
- }));
-};
-
-upRule.prototype.upSymbolizers = function(xmlRule) {
- var css_rules = [];
- var symnames = _.map(_.keys(mess.tree.Reference.data.symbolizers), function(symbolizer) {
- return [symbolizer.charAt(0).toUpperCase() +
- symbolizer.slice(1).replace(/\-./, function(str) {
- return str[1].toUpperCase();
- }) + 'Symbolizer', mess.tree.Reference.data.symbolizers[symbolizer]];
- });
- var symmap = _.reduce(symnames, function(memo, s) {
- memo[s[0]] = s[1];
- return memo;
- }, {});
- var cssmap = function(symbolizer, name) {
- return symmap[symbolizer][name].css;
- }
- for (var i in xmlRule) {
- if (i in symmap) {
- for (var j in xmlRule[i]['@']) {
- if (symmap[i][j].type == 'uri') {
- css_rules.push(cssmap(i, j) + ': url("' + xmlRule[i]['@'][j] + '");');
- } else {
- css_rules.push(cssmap(i, j) + ': "' + xmlRule[i]['@'][j] + '";');
- }
- }
- }
- }
- return css_rules;
-};
-
-
-upRule.prototype.toMSS = function() {
- return ' ' + this.filters.map(function(f) {
- return '[' + f + ']';
- }).join('')
- + ' {\n '
- + this.rules.join('\n ')
- + '\n }';
-}
-
-fs.readFile(input, 'utf-8', function (e, data) {
- var styles = [];
- var firstParser = new xml2js.Parser();
- firstParser.addListener('end', function(mapnik_xml) {
- mapnik_xml.Style.forEach(function(s) {
- var newStyle = new upStyle(s);
- s.Rule.forEach(function(r) {
- newStyle.rules.push((new upRule(r)).toMSS());
- });
- styles.push(newStyle.toMSS());
- });
- console.log(styles.join('\n'));
- }).parseString(data);
-});
diff --git a/bin/messc b/bin/messc
deleted file mode 100755
index 78daee4..0000000
--- a/bin/messc
+++ /dev/null
@@ -1,101 +0,0 @@
-#!/usr/bin/env node
-
-var path = require('path'),
- fs = require('fs'),
- sys = require('sys');
-
-require.paths.unshift(path.join(__dirname, '../lib'), path.join(__dirname, '../lib/node'));
-
-var mess = require('mess');
-var args = process.argv.slice(1);
-var options = {
- silent: false,
- json: false
-};
-
-args = args.filter(function (arg) {
- var match;
-
- if (match = arg.match(/^--?([a-z][0-9a-z-]*)$/i)) { arg = match[1] }
- else { return arg }
-
- switch (arg) {
- case 'v':
- case 'version':
- sys.puts("messc " + mess.version.join('.') + " (MESS Compiler) [JavaScript]");
- process.exit(0);
- break;
- case 'verbose':
- options.verbose = true;
- break;
- case 'd':
- case 'debug':
- options.debug = true;
- break;
- case 's':
- case 'silent':
- options.silent = true;
- break;
- case 'b':
- case 'benchmark':
- options.benchmark = true;
- break;
- case 'h':
- case 'help':
- sys.puts("Usage: messc source");
- sys.puts("Options:");
- sys.puts(" -j\tParse JSON map manifest");
- process.exit(0);
- break;
- }
-});
-
-var input = args[1];
-if (input && input[0] != '/') {
- input = path.join(process.cwd(), input);
-}
-var output = args[2];
-if (output && output[0] != '/') {
- output = path.join(process.cwd(), output);
-}
-
-if (!input) {
- sys.puts("messc: no input files");
- process.exit(1);
-}
-
-if (options.benchmark) {
- var start = +new Date;
-}
-
-fs.readFile(input, 'utf-8', function (e, data) {
- if (e) {
- sys.puts("messc: " + e.message);
- process.exit(1);
- }
-
- new mess.Renderer({
- filename: input,
- debug: options.debug,
- local_data_dir: path.dirname(input),
- }).render(data, function(err, output) {
- if (err) {
- if (Array.isArray(err)) {
- err.forEach(function(e) {
- mess.writeError(e, options);
- });
- } else {
- throw err;
- }
-
- process.exit(1);
- } else {
- if (!options.benchmark) {
- sys.puts(output);
- } else {
- var duration = (+new Date) - start;
- console.log('Benchmark: ' + (duration) + 'ms');
- }
- }
- });
-});
diff --git a/lib/carto/external.js b/lib/carto/external.js
new file mode 100644
index 0000000..7cb0ffa
--- /dev/null
+++ b/lib/carto/external.js
@@ -0,0 +1,318 @@
+var fs = require('fs'),
+ get = require('node-get'),
+ url = require('url'),
+ sys = require('sys'),
+ EventEmitter = require('events').EventEmitter,
+ path = require('path'),
+ crypto = require('crypto'),
+ assert = require('assert'),
+ zip = require('zipfile'),
+ Step = require('step');
+
+// node compatibility for mkdirs below
+var constants = (!process.EEXIST >= 1) ?
+ require('constants') :
+ { EEXIST: process.EEXIST };
+
+
+function External(env, uri) {
+ var local = !(/^https?:\/\//i.test(uri));
+ if (local) {
+ uri = path.join(env.local_data_dir, uri);
+ }
+
+ if (External.instances[uri]) return External.instances[uri];
+
+ this.uri = uri;
+ this.env = env;
+ this.format = path.extname(uri).toLowerCase();
+ this.type = External.findType(this.format);
+ this._callbacks = [];
+ this.done = false;
+
+ External.mkdirp(this.env.data_dir, 0755);
+
+ if (local) {
+ this.localFile();
+ } else {
+ this.downloadFile();
+ }
+
+ External.instances[this.uri] = this;
+}
+sys.inherits(External, EventEmitter);
+
+External.instances = {};
+
+External.prototype.invokeCallbacks = function(err) {
+ delete External.instances[this.uri];
+
+ if (err) {
+ this.emit('err', err);
+ } else {
+ this.emit('complete', this);
+ }
+};
+
+
+External.prototype.localFile = function() {
+ // Only treat files as local that don't have to be processed.
+ this.isLocal = !External.processors[this.format];
+
+ this.tempPath = this.uri;
+ fs.stat(this.tempPath, this.processFile.bind(this));
+};
+
+External.prototype.downloadFile = function() {
+ this.tempPath = path.join(
+ this.env.data_dir,
+ crypto.createHash('md5').update(this.uri).digest('hex')
+ + path.extname(this.uri));
+
+ fs.stat(this.path(), function(err, stats) {
+ if (err) {
+ // This file does not yet exist. Download it!
+ (new get(this.uri)).toDisk(
+ this.tempPath,
+ this.processFile.bind(this));
+ } else {
+ this.invokeCallbacks(null);
+ }
+ }.bind(this));
+};
+
+External.prototype.processFile = function(err) {
+ if (err) {
+ this.invokeCallbacks(err);
+ } else {
+ if (this.isLocal) {
+ this.invokeCallbacks(null);
+ } else {
+ (External.processors[this.format] || External.processors['default'])(
+ this.tempPath,
+ this.path(),
+ function(err) {
+ if (err) {
+ this.invokeCallbacks(err);
+ } else {
+ this.invokeCallbacks(null);
+ }
+ }.bind(this)
+ );
+ }
+ }
+};
+
+External.prototype.path = function() {
+ if (this.isLocal) {
+ return this.tempPath;
+ } else {
+ return path.join(
+ this.env.data_dir,
+ (External.destinations[this.format] ||
+ External.destinations['default'])(this.uri)
+ );
+ }
+};
+
+External.prototype.findDataFile = function(callback) {
+ this.type.datafile(this, callback);
+};
+
+External.prototype.findOneByExtension = function(ext, callback) {
+ var cb = function(err, files) { callback(null, files.pop()); }
+ this.findByExtension(ext, cb);
+}
+
+External.prototype.findByExtension = function(ext, callback) {
+ var running = 0;
+ var found = [];
+
+ (function find(dir) {
+ running++;
+ fs.readdir(dir, function(err, files) {
+ if (err) {
+ running = 0;
+ callback(err);
+ return;
+ i;
+ }
+ files.forEach(function(file) {
+ running++;
+ file = path.join(dir, file);
+ fs.stat(file, function(err, stats) {
+ if (err) {
+ running = 0;
+ callback(err);
+ return;
+ }
+ if (stats.isDirectory()) {
+ find(file);
+ } else if (stats.isFile() && path.extname(file) === ext) {
+ found.push(file);
+ }
+ if (running && !--running) callback(null, found);
+ });
+ });
+ if (running && !--running) callback(null, found);
+ });
+ })(this.path());
+};
+
+// https://gist.github.com/707661
+External.mkdirp = function mkdirP(p, mode, f) {
+ var cb = f || function() {};
+ if (p.charAt(0) != '/') {
+ cb('Relative path: ' + p);
+ return;
+ }
+
+ var ps = path.normalize(p).split('/');
+ path.exists(p, function(exists) {
+ if (exists) cb(null);
+ else mkdirP(ps.slice(0, -1).join('/'), mode, function(err) {
+ if (err && err.errno != process.EEXIST) cb(err);
+ else {
+ fs.mkdir(p, mode, cb);
+ }
+ });
+ });
+};
+
+External.types = [
+ {
+ extension: /\.zip/,
+ datafile: function(d, c) { d.findOneByExtension('.shp', c) },
+ ds_options: {
+ type: 'shape'
+ }
+ },
+ {
+ extension: /\.shp/,
+ datafile: function(d, c) { c(null, d.path()) },
+ ds_options: {
+ type: 'shape'
+ }
+ },
+ {
+ extension: /\.png/,
+ datafile: function(d, c) { c(null, d.path()) },
+ },
+ {
+ extension: /\.jpe?g/,
+ },
+ {
+ extension: /\.geotiff?|\.tiff?/,
+ datafile: function(d, c) { c(null, d.path()) },
+ ds_options: {
+ type: 'gdal'
+ }
+ },
+ {
+ extension: /\.vrt/,
+ datafile: function(d, c) { c(null, d.path()) },
+ ds_options: {
+ type: 'gdal'
+ }
+ },
+ {
+ extension: /\.kml/,
+ datafile: function(d, c) { c(null, d.path()) },
+ ds_options: {
+ type: 'ogr',
+ layer_by_index: 0
+ }
+ },
+ {
+ extension: /\.geojson/,
+ datafile: function(d, c) { c(null, d.path()) },
+ ds_options: {
+ type: 'ogr',
+ layer_by_index: 0
+ }
+ },
+ {
+ extension: /\.rss/,
+ datafile: function(d, c) { c(d.path()) },
+ ds_options: {
+ type: 'ogr',
+ layer_by_index: 0
+ }
+ },
+ {
+ extension: /.*/g,
+ datafile: function(d, c) { c(d.path()) },
+ }
+];
+
+External.findType = function(ext) {
+ for (var i in External.types) {
+ if (ext.match(External.types[i].extension)) {
+ return External.types[i];
+ }
+ }
+};
+
+// Destinations are names in the data_dir/cache directory.
+External.destinations = {};
+External.destinations['default'] = function(uri) {
+ return crypto.createHash('md5').update(uri).digest('hex') + path.extname(uri);
+};
+External.destinations['.zip'] = function(uri) {
+ return crypto.createHash('md5').update(uri).digest('hex');
+};
+
+External.processors = {};
+External.processors['default'] = function(tempPath, destPath, callback) {
+ if (tempPath === destPath) {
+ callback(null);
+ } else {
+ fs.rename(tempPath, destPath, callback);
+ }
+};
+
+External.processors['.zip'] = function(tempPath, destPath, callback) {
+ try {
+ console.log('unzipping file');
+ var zf = new zip.ZipFile(tempPath);
+ } catch (err) {
+ callback(err);
+ return;
+ }
+
+ Step(
+ function() {
+ var group = this.group();
+ zf.names.forEach(function(name) {
+ var next = group();
+ var uncompressed = path.join(destPath, name);
+ External.mkdirp(path.dirname(uncompressed), 0755, function(err) {
+ if (err && err.errno != constants.EEXIST) {
+ callback("Couldn't create directory " + path.dirname(name));
+ }
+ else {
+ // if just a directory skip ahead
+ if (!path.extname(name)) {
+ next();
+ } else {
+ var buffer = zf.readFile(name, function(err, buffer) {
+ fd = fs.open(uncompressed, 'w', 0755, function(err, fd) {
+ sys.debug('saving to: ' + uncompressed);
+ fs.write(fd, buffer, 0, buffer.length, null,
+ function(err, written) {
+ fs.close(fd, function(err) {
+ next();
+ });
+ });
+ });
+ });
+ }
+ }
+ });
+ });
+ },
+ callback
+ );
+};
+
+module.exports = External;
diff --git a/lib/carto/functions.js b/lib/carto/functions.js
new file mode 100644
index 0000000..b7b3312
--- /dev/null
+++ b/lib/carto/functions.js
@@ -0,0 +1,157 @@
+(function (tree) {
+
+tree.functions = {
+ rgb: function (r, g, b) {
+ return this.rgba(r, g, b, 1.0);
+ },
+ rgba: function (r, g, b, a) {
+ var rgb = [r, g, b].map(function (c) { return number(c) }),
+ a = number(a);
+ return new tree.Color(rgb, a);
+ },
+ hsl: function (h, s, l) {
+ return this.hsla(h, s, l, 1.0);
+ },
+ hsla: function (h, s, l, a) {
+ h = (number(h) % 360) / 360;
+ s = number(s); l = number(l); a = number(a);
+
+ var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;
+ var m1 = l * 2 - m2;
+
+ return this.rgba(hue(h + 1/3) * 255,
+ hue(h) * 255,
+ hue(h - 1/3) * 255,
+ a);
+
+ function hue(h) {
+ h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h);
+ if (h * 6 < 1) return m1 + (m2 - m1) * h * 6;
+ else if (h * 2 < 1) return m2;
+ else if (h * 3 < 2) return m1 + (m2 - m1) * (2/3 - h) * 6;
+ else return m1;
+ }
+ },
+ hue: function (color) {
+ return new tree.Dimension(Math.round(color.toHSL().h));
+ },
+ saturation: function (color) {
+ return new tree.Dimension(Math.round(color.toHSL().s * 100), '%');
+ },
+ lightness: function (color) {
+ return new tree.Dimension(Math.round(color.toHSL().l * 100), '%');
+ },
+ alpha: function (color) {
+ return new tree.Dimension(color.toHSL().a);
+ },
+ saturate: function (color, amount) {
+ var hsl = color.toHSL();
+
+ hsl.s += amount.value / 100;
+ hsl.s = clamp(hsl.s);
+ return hsla(hsl);
+ },
+ desaturate: function (color, amount) {
+ var hsl = color.toHSL();
+
+ hsl.s -= amount.value / 100;
+ hsl.s = clamp(hsl.s);
+ return hsla(hsl);
+ },
+ lighten: function (color, amount) {
+ var hsl = color.toHSL();
+
+ hsl.l += amount.value / 100;
+ hsl.l = clamp(hsl.l);
+ return hsla(hsl);
+ },
+ darken: function (color, amount) {
+ var hsl = color.toHSL();
+
+ hsl.l -= amount.value / 100;
+ hsl.l = clamp(hsl.l);
+ return hsla(hsl);
+ },
+ fadein: function (color, amount) {
+ var hsl = color.toHSL();
+
+ hsl.a += amount.value / 100;
+ hsl.a = clamp(hsl.a);
+ return hsla(hsl);
+ },
+ fadeout: function (color, amount) {
+ var hsl = color.toHSL();
+
+ hsl.a -= amount.value / 100;
+ hsl.a = clamp(hsl.a);
+ return hsla(hsl);
+ },
+ spin: function (color, amount) {
+ var hsl = color.toHSL();
+ var hue = (hsl.h + amount.value) % 360;
+
+ hsl.h = hue < 0 ? 360 + hue : hue;
+
+ return hsla(hsl);
+ },
+ //
+ // Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein
+ // http://sass-lang.com
+ //
+ mix: function (color1, color2, weight) {
+ var p = weight.value / 100.0;
+ var w = p * 2 - 1;
+ var a = color1.toHSL().a - color2.toHSL().a;
+
+ var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0;
+ var w2 = 1 - w1;
+
+ var rgb = [color1.rgb[0] * w1 + color2.rgb[0] * w2,
+ color1.rgb[1] * w1 + color2.rgb[1] * w2,
+ color1.rgb[2] * w1 + color2.rgb[2] * w2];
+
+ var alpha = color1.alpha * p + color2.alpha * (1 - p);
+
+ return new tree.Color(rgb, alpha);
+ },
+ greyscale: function (color) {
+ return this.desaturate(color, new tree.Dimension(100));
+ },
+ e: function (str) {
+ return new tree.Anonymous(str instanceof tree.JavaScript ? str.evaluated : str);
+ },
+ '%': function (quoted /* arg, arg, ...*/) {
+ var args = Array.prototype.slice.call(arguments, 1),
+ str = quoted.value;
+
+ for (var i = 0; i < args.length; i++) {
+ str = str.replace(/%s/, args[i].value)
+ .replace(/%[da]/, args[i].toString());
+ }
+ str = str.replace(/%%/g, '%');
+ return new tree.Quoted('"' + str + '"', str);
+ }
+};
+
+function hsla(hsla) {
+ return tree.functions.hsla(hsla.h, hsla.s, hsla.l, hsla.a);
+}
+
+function number(n) {
+ if (n instanceof tree.Dimension) {
+ return parseFloat(n.unit == '%' ? n.value / 100 : n.value);
+ } else if (typeof(n) === 'number') {
+ return n;
+ } else {
+ throw {
+ error: "RuntimeError",
+ message: "color functions take numbers as parameters"
+ };
+ }
+}
+
+function clamp(val) {
+ return Math.min(1, Math.max(0, val));
+}
+
+})(require('carto/tree'));
diff --git a/lib/carto/index.js b/lib/carto/index.js
new file mode 100644
index 0000000..b2e991b
--- /dev/null
+++ b/lib/carto/index.js
@@ -0,0 +1,118 @@
+var path = require('path'),
+ sys = require('sys'),
+ fs = require('fs');
+
+require.paths.unshift(path.join(__dirname, '..'));
+
+var carto = {
+ version: [1, 0, 40],
+ Parser: require('carto/parser').Parser,
+ Renderer: require('carto/renderer').Renderer,
+ External: require('carto/external'),
+ importer: require('carto/parser').importer,
+ tree: require('carto/tree'),
+ writeError: function(ctx, options) {
+ var message = '';
+ var extract = ctx.extract;
+ var error = [];
+
+ options = options || {};
+
+ if (options.silent) { return }
+
+ options.indent = options.indent || '';
+
+ if (!('index' in ctx) || !extract) {
+ return sys.error(options.indent + (ctx.stack || ctx.message));
+ }
+
+ if (typeof(extract[0]) === 'string') {
+ error.push(stylize((ctx.line - 1) + ' ' + extract[0], 'grey'));
+ }
+
+ if (extract[1] === '' && typeof extract[2] === 'undefined') {
+ extract[1] = '¶';
+ }
+ error.push(ctx.line + ' ' + extract[1].slice(0, ctx.column)
+ + stylize(stylize(extract[1][ctx.column], 'bold')
+ + extract[1].slice(ctx.column + 1), 'yellow'));
+
+ if (typeof(extract[2]) === 'string') {
+ error.push(stylize((ctx.line + 1) + ' ' + extract[2], 'grey'));
+ }
+ error = options.indent + error.join('\n' + options.indent) + '\033[0m\n';
+
+ message = options.indent + message + stylize(ctx.message, 'red');
+ ctx.filename && (message += stylize(' in ', 'red') + ctx.filename);
+
+ sys.error(message, error);
+
+ if (ctx.callLine) {
+ sys.error(stylize('from ', 'red') + (ctx.filename || ''));
+ sys.error(stylize(ctx.callLine, 'grey') + ' ' + ctx.callExtract);
+ }
+ if (ctx.stack) { sys.error(stylize(ctx.stack, 'red')) }
+ }
+};
+
+[ 'alpha', 'anonymous', 'call', 'color', 'comment', 'definition', 'dimension',
+ 'directive', 'element', 'expression', 'filterset', 'filter',
+ 'import', 'javascript', 'keyword', 'layer', 'mixin', 'operation', 'quoted',
+ 'reference', 'rule', 'ruleset', 'selector', 'style', 'url', 'value',
+ 'variable', 'zoom', 'invalid', 'fontset'
+].forEach(function(n) {
+ require(path.join('carto', 'tree', n));
+});
+
+carto.Parser.importer = function(file, paths, callback) {
+ var pathname;
+
+ paths.unshift('.');
+
+ for (var i = 0; i < paths.length; i++) {
+ try {
+ pathname = path.join(paths[i], file);
+ fs.statSync(pathname);
+ break;
+ } catch (e) {
+ pathname = null;
+ }
+ }
+
+ if (pathname) {
+ fs.readFile(pathname, 'utf-8', function(e, data) {
+ if (e) sys.error(e);
+
+ new carto.Parser({
+ paths: [path.dirname(pathname)],
+ filename: pathname
+ }).parse(data, function(e, root) {
+ if (e) carto.writeError(e);
+ callback(root);
+ });
+ });
+ } else {
+ sys.error("file '" + file + "' wasn't found.\n");
+ process.exit(1);
+ }
+};
+
+require('carto/functions');
+
+for (var k in carto) { exports[k] = carto[k] }
+
+// Stylize a string
+function stylize(str, style) {
+ var styles = {
+ 'bold' : [1, 22],
+ 'inverse' : [7, 27],
+ 'underline' : [4, 24],
+ 'yellow' : [33, 39],
+ 'green' : [32, 39],
+ 'red' : [31, 39],
+ 'grey' : [90, 39]
+ };
+ return '\033[' + styles[style][0] + 'm' + str +
+ '\033[' + styles[style][1] + 'm';
+}
+
diff --git a/lib/carto/parser.js b/lib/carto/parser.js
new file mode 100644
index 0000000..6a215d4
--- /dev/null
+++ b/lib/carto/parser.js
@@ -0,0 +1,994 @@
+var carto, tree;
+
+if (typeof(window) === 'undefined') {
+ carto = exports,
+ tree = require('carto/tree');
+} else {
+ if (typeof(window.carto) === 'undefined') { window.carto = {} }
+ carto = window.carto,
+ tree = window.carto.tree = {};
+}
+//
+// carto.js - parser
+//
+// A relatively straight-forward predictive parser.
+// There is no tokenization/lexing stage, the input is parsed
+// in one sweep.
+//
+// To make the parser fast enough to run in the browser, several
+// optimization had to be made:
+//
+// - Matching and slicing on a huge input is often cause of slowdowns.
+// The solution is to chunkify the input into smaller strings.
+// The chunks are stored in the `chunks` var,
+// `j` holds the current chunk index, and `current` holds
+// the index of the current chunk in relation to `input`.
+// This gives us an almost 4x speed-up.
+//
+// - In many cases, we don't need to match individual tokens;
+// for example, if a value doesn't hold any variables, operations
+// or dynamic references, the parser can effectively 'skip' it,
+// treating it as a literal.
+// An example would be '1px solid #000' - which evaluates to itself,
+// we don't need to know what the individual components are.
+// The drawback, of course is that you don't get the benefits of
+// syntax-checking on the CSS. This gives us a 50% speed-up in the parser,
+// and a smaller speed-up in the code-gen.
+//
+//
+// 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
+ furthest, // furthest index the parser has gone to
+ chunks, // chunkified input
+ current, // index of current chunk, in `input`
+ parser;
+
+ var that = this;
+
+ // This function is called after all files
+ // have been imported through `@import`.
+ var finish = function() {};
+
+ var imports = this.imports = {
+ paths: env && env.paths || [], // Search paths, when importing
+ queue: [], // Files which haven't been imported yet
+ files: {}, // Holds the imported parse trees
+ mime: env && env.mime, // MIME type of .carto files
+ push: function(path, callback) {
+ var that = this;
+ this.queue.push(path);
+
+ //
+ // Import a file asynchronously
+ //
+ carto.Parser.importer(path, this.paths, function(root) {
+ that.queue.splice(that.queue.indexOf(path), 1); // Remove the path from the queue
+ that.files[path] = root; // Store the root
+
+ callback(root);
+
+ if (that.queue.length === 0) { finish() } // Call `finish` if we're done importing
+ }, env);
+ }
+ };
+
+ 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, args, length, c, index, endIndex, k;
+
+ //
+ // 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();
+
+ if (match = tok.exec(chunks[j])) {
+ 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) {
+ 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 {
+ if (tok.test(chunks[j])) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ function errorMessage(message, i) {
+ if (typeof i === 'undefined') i = furthest;
+ lines = input.split('\n');
+ line = (input.slice(0, i).match(/\n/g) || '').length + 1;
+
+ for (var n = i, column = -1; n >= 0 && input.charAt(n) !== '\n'; n--) { column++ }
+
+ return {
+ name: 'ParseError',
+ message: (message || 'Syntax Error') + ' on line ' + line,
+ filename: env.filename,
+ line: line,
+ index: i,
+ column: column,
+ extract: [
+ lines[line - 2],
+ lines[line - 1],
+ lines[line]
+ ]
+ };
+ }
+
+ this.env = env = env || {};
+ this.env.filename = this.env.filename || null;
+ this.env.error = function(e) {
+ if (!env.errors) env.errors = [];
+ env.errors.push(e);
+ };
+
+ //
+ // The Parser
+ //
+ return parser = {
+
+ imports: imports,
+ //
+ // Parse an input string into an abstract syntax tree,
+ // call `callback` when done.
+ //
+ parse: function(str, callback) {
+ var root, start, end, zone, line, lines, buff = [], c, error = null;
+
+ i = j = current = furthest = 0;
+ chunks = [];
+ input = str.replace(/\r\n/g, '\n');
+
+ var early_exit = false;
+ // Split the input into chunks.
+ chunks = (function(chunks) {
+ var j = 0,
+ skip = /[^"'`\{\}\/]+/g,
+ comment = /\/\*(?:[^*]+|\*+[^\/*])*\*+\/|\/\/.*/g,
+ level = 0,
+ match,
+ chunk = chunks[0],
+ inString;
+
+ chunker: for (var i = 0, c, cc; i < input.length; i++) {
+ skip.lastIndex = i;
+ if (match = skip.exec(input)) {
+ if (match.index === i) {
+ i += match[0].length;
+ chunk.push(match[0]);
+ }
+ }
+ c = input.charAt(i);
+ comment.lastIndex = i;
+
+ if (!inString && c === '/') {
+ cc = input.charAt(i + 1);
+ if (cc === '/' || cc === '*') {
+ if (match = comment.exec(input)) {
+ if (match.index === i) {
+ i += match[0].length - 1;
+ chunk.push(match[0]);
+ c = input.charAt(i);
+ continue chunker;
+ }
+ }
+ }
+ }
+
+ if (c === '{' && !inString) { level++;
+ chunk.push(c);
+ } else if (c === '}' && !inString) { level--;
+ chunk.push(c);
+ chunks[++j] = chunk = [];
+ } else {
+ if (c === '"' || c === "'" || c === '`') {
+ if (! inString) {
+ inString = c;
+ } else {
+ inString = inString === c ? false : inString;
+ }
+ }
+ chunk.push(c);
+ }
+ }
+ if (level > 0) {
+ // TODO: make invalid instead
+ callback([{
+ index: i,
+ line: 0,
+ filename: env.filename,
+ message: 'Missing closing `}`'
+ }]);
+ early_exit = true;
+ }
+
+ return chunks.map(function(c) { return c.join('') });
+ })([[]]);
+
+ // callback has been called, chunker failed so that this isn't doable.
+ if (early_exit) return;
+
+ // 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. The callback is called when the input is parsed.
+ root = new tree.Ruleset([], $(this.parsers.primary));
+ root.root = true;
+
+ root.getLine = function(index) {
+ return index ? (input.slice(0, index).match(/\n/g) || '').length : null;
+ };
+
+ root.makeError = function(e) {
+ lines = input.split('\n');
+ line = root.getLine(e.index);
+
+ for (var n = e.index, column = -1;
+ n >= 0 && input.charAt(n) !== '\n';
+ n--) { column++ }
+
+ return {
+ type: e.type,
+ message: e.message,
+ filename: e.filename,
+ index: e.index,
+ line: typeof(line) === 'number' ? line + 1 : null,
+ column: column,
+ extract: [
+ lines[line - 1],
+ lines[line],
+ lines[line + 1]
+ ]
+ }
+ }
+
+ // Get an array of Ruleset objects, flattened
+ // and sorted according to specificitySort
+ root.toList = (function() {
+ var line, lines, column, _ = require('underscore')._;
+ return function(env) {
+ env.error = function(e) {
+ if (!env.errors) env.errors = [];
+ env.errors.push(e);
+ };
+ env.errors = [];
+ env.frames = env.frames || [];
+
+ // call populates Invalid-caused errors
+ var definitions = this.flatten([], [], env);
+ definitions.sort(specificitySort);
+ env.errors = env.errors.map(function(e) {
+ _.extend(e, root.makeError(e));
+ return e;
+ });
+ 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];
+ };
+
+ // If `i` is smaller than the `input.length - 1`,
+ // it means the parser wasn't able to parse the whole
+ // string, so we've got a parsing error.
+ //
+ // We try to extract a \n delimited string,
+ // showing the line where the parse error occured.
+ // We split it up into two parts (the part which parsed,
+ // and the part which didn't), so we can color them differently.
+ if (i < input.length - 1) {
+ error = errorMessage('Parse error', i);
+ }
+
+ callback(error, root);
+ },
+
+ //
+ // Here in, the parsing rules/functions
+ //
+ // The basic structure of the syntax tree generated is as follows:
+ //
+ // Ruleset -> Rule -> Value -> Expression -> Entity
+ //
+ // Here's some LESS code:
+ //
+ // .class {
+ // color: #fff;
+ // border: 1px solid #000;
+ // width: @w + 4px;
+ // > .child {...}
+ // }
+ //
+ // And here's what the parse tree might look like:
+ //
+ // Ruleset (Selector '.class', [
+ // Rule ("color", Value ([Expression [Color #fff]]))
+ // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]]))
+ // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]]))
+ // Ruleset (Selector [Element '>', '.child'], [...])
+ // ])
+ //
+ // 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.mixin.definition) || $(this.rule) || $(this.ruleset) ||
+ $(this.mixin.call) || $(this.comment))
+ || $(/^[\s\n]+/) || (node = $(this.invalid))) {
+ node && root.push(node);
+ }
+ return root;
+ },
+
+ invalid: function () {
+ var chunk;
+
+ // To fail gracefully, match everything until a semicolon or linebreak.
+ if (chunk = $(/^[^;\n]*[;\n]/)) {
+ return new(tree.Invalid)(chunk, memo);
+ }
+ },
+
+ // We create a Comment node for CSS comments `/* */`,
+ // but keep the LeSS comments `//` silent, by just skipping
+ // over them.
+ comment: function() {
+ var comment;
+
+ if (input.charAt(i) !== '/') return;
+
+ if (input.charAt(i + 1) === '/') {
+ return new tree.Comment($(/^\/\/.*/), true);
+ } else if (comment = $(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/)) {
+ 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() {
+ var str;
+ if (input.charAt(i) !== '"' && input.charAt(i) !== "'") return;
+
+ if (str = $(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/)) {
+ return new tree.Quoted(str[0], str[1] || str[2]);
+ }
+ },
+
+ comparison: function() {
+ var str;
+ if (str = $(/^=|!=|<=|>=|<|>/)) {
+ return str;
+ }
+ },
+
+ //
+ // A catch-all word, such as:
+ //
+ // black border-collapse
+ //
+ keyword: function() {
+ var k;
+ if (k = $(/^[A-Za-z-]+[A-Za-z-0-9]*/)) { return new tree.Keyword(k) }
+ },
+
+ //
+ // A function call
+ //
+ // 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].toLowerCase();
+
+ if (name === 'url') { return null }
+ else { i += name.length + 1 }
+
+ args = $(this.entities.arguments);
+
+ if (! $(')')) return;
+
+ if (name) { return new tree.Call(name, args) }
+ },
+ arguments: function() {
+ var args = [], arg;
+
+ while (arg = $(this.expression)) {
+ args.push(arg);
+ if (! $(',')) { break }
+ }
+ return args;
+ },
+ literal: function() {
+ return $(this.entities.dimension) ||
+ $(this.entities.color) ||
+ $(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.');
+ } else {
+ return new tree.URL((value.value || value.data || value instanceof tree.Variable)
+ ? value : new tree.Anonymous(value), imports.paths);
+ }
+ },
+
+ //
+ // 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);
+ }
+ },
+
+ //
+ // A Hexadecimal color
+ //
+ // #4F3C2F
+ //
+ // `rgb` and `hsl` colors are parsed through the `entities.call` parser.
+ //
+ color: function() {
+ var rgb;
+
+ if (input.charAt(i) === '#' && (rgb = $(/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})/))) {
+ return new tree.Color(rgb[1]);
+ }
+ },
+
+ //
+ // A Dimension, that is, a number and a unit
+ //
+ // 0.5em 95%
+ //
+ dimension: function() {
+ var value, c = input.charCodeAt(i);
+ if ((c > 57 || c < 45) || c === 47) return;
+
+ if (value = $(/^(-?\d*\.?\d+)(px|%|em|pc|ex|in|deg|s|ms|pt|cm|mm|rad|grad|turn)?/)) {
+ return new tree.Dimension(value[1], value[2], memo);
+ }
+ },
+
+ //
+ // JavaScript code to be evaluated
+ //
+ // `window.location.href`
+ //
+ javascript: function() {
+ var str;
+
+ if (input.charAt(i) !== '`') { return }
+
+ if (str = $(/^`([^`]*)`/)) {
+ return new tree.JavaScript(str[1], i);
+ }
+ }
+ },
+
+ //
+ // The variable part of a variable definition. Used in the `rule` parser
+ //
+ // @fink:
+ //
+ variable: function() {
+ var name;
+
+ if (input.charAt(i) === '@' && (name = $(/^(@[\w-]+)\s*:/))) { return name[1] }
+ },
+
+ //
+ // Mixins
+ //
+ mixin: {
+ //
+ // A Mixin call, with an optional argument list
+ //
+ // #mixins > .square(#fff);
+ // .rounded(4px, black);
+ // .button;
+ //
+ // The `while` loop is there because mixins can be
+ // namespaced, but we only support the child and descendant
+ // selector for now.
+ //
+ call: function() {
+ var elements = [], e, c, args, index = i, s = input.charAt(i);
+
+ if (s !== '.' && s !== '#') { return }
+
+ while (e = $(/^[#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+/)) {
+ elements.push(new tree.Element(c, e));
+ c = $('>');
+ }
+ $('(') && (args = $(this.entities.arguments)) && $(')');
+
+ if (elements.length > 0 && ($(';') || peek('}'))) {
+ throw 'Calls are not yet supported';
+ return new tree.mixin.Call(elements, args, index);
+ }
+ },
+
+ //
+ // A Mixin definition, with a list of parameters
+ //
+ // .rounded (@radius: 2px, @color) {
+ // ...
+ // }
+ //
+ // Until we have a finer grained state-machine, we have to
+ // do a look-ahead, to make sure we don't have a mixin call.
+ // See the `rule` function for more information.
+ //
+ // We start by matching `.rounded (`, and then proceed on to
+ // the argument list, which has optional default values.
+ // We store the parameters in `params`, with a `value` key,
+ // if there is a value, such as in the case of `@radius`.
+ //
+ // Once we've got our params list, and a closing `)`, we parse
+ // the `{...}` block.
+ //
+ definition: function() {
+ var name, params = [], match, ruleset, param, value;
+
+ if ((input.charAt(i) !== '.' && input.charAt(i) !== '#') ||
+ peek(/^[^{]*(;|})/)) return;
+
+ if (match = $(/^([#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+)[\s,]*\(/)) {
+ name = match[1];
+
+ while (param = $(this.entities.variable) || $(this.entities.literal)
+ || $(this.entities.keyword)) {
+ // Variable
+ if (param instanceof tree.Variable) {
+ if ($(':')) {
+ if (value = $(this.expression)) {
+ params.push({ name: param.name, value: value });
+ } else {
+ throw new Error('Expected value');
+ }
+ } else {
+ params.push({ name: param.name });
+ }
+ } else {
+ params.push({ value: param });
+ }
+ if (! $(',')) { break }
+ }
+ if (! $(')')) throw new Error('Expected )');
+
+ ruleset = $(this.block);
+
+ if (ruleset) {
+ throw 'Definitions should not exist here';
+ return new tree.mixin.Definition(name, params, ruleset);
+ }
+ }
+ }
+ },
+
+ //
+ // Entities are the smallest recognized token,
+ // and can be found inside a rule's value.
+ //
+ entity: function() {
+ return $(this.entities.literal) || $(this.entities.variable) || $(this.entities.url) ||
+ $(this.entities.call) || $(this.entities.keyword) || $(this.entities.javascript);
+ },
+
+ //
+ // 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('}');
+ },
+
+ //
+ // A Selector Element
+ //
+ // div
+ // .classname
+ // #socks
+ // input[type="text"]
+ //
+ // Elements are the building blocks for Selectors. They consist of
+ // an element name, such as a tag a class, or `*`.
+ //
+ element: function() {
+ var e;
+ if (e = $(/^(?:[.#]?[\w-]+|\*)/)) {
+ 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;
+ if (s = $(/^::([\w-]+(?:\/[\w-]+)*)/)) {
+ // There's no object for attachment names.
+ return s[1];
+ }
+ },
+
+ //
+ // A CSS Selector
+ //
+ // .class > div + h1
+ // li a:hover
+ //
+ // Selectors are made out of one or more Elements, see above.
+ //
+ selector: function() {
+ var a, attachment;
+ var e, elements = [];
+ var f, filters = new tree.Filterset();
+ var z, zoom = tree.Zoom.all;
+ var 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) {
+ zoom &= z;
+ conditions++;
+ } else if (f) {
+ filters.add(f);
+ conditions++;
+ } else if (attachment) {
+ throw errorMessage('Encountered second attachment name', i - 1);
+ } else {
+ attachment = a;
+ }
+
+ var c = input.charAt(i);
+ if (c === '{' || c === '}' || c === ';' || c === ',') { break }
+ }
+
+ if (segments) {
+ return new tree.Selector(filters, zoom, elements, attachment, conditions, memo);
+ }
+ },
+
+ filter: function() {
+ save();
+ var key, op, val;
+ if (! $('[')) return;
+ if (key = $(/^[a-zA-Z0-9-_]+/) || $(this.entities.quoted)) {
+ if ((op = $(this.entities.comparison)) &&
+ (val = $(this.entities.quoted) || $(/^[\w-\.]+/))) {
+ if (! $(']')) return;
+ return new tree.Filter(key, op, val, memo);
+ }
+ }
+ },
+
+ zoom: function() {
+ save();
+ var op, val;
+ if ($(/^\[zoom/g) &&
+ (op = $(this.entities.comparison)) &&
+ (val = $(/^\d+/)) &&
+ $(']')) {
+ return tree.Zoom(op, val, memo);
+ }
+ },
+
+ //
+ // The `block` rule is used by `ruleset` and `mixin.definition`.
+ // 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, f, l, rules, filters = [];
+ save();
+
+ while (s = $(this.selector)) {
+ selectors.push(s);
+ if (! $(',')) { break }
+ }
+ if (s) $(this.comment);
+
+ 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 === '#' || c === '&') { return }
+
+ if (name = $(this.variable) || $(this.property)) {
+ value = $(this.value);
+
+ if (value && $(this.end)) {
+ return new tree.Rule(name, value, memo, env.filename);
+ } else {
+ furthest = i;
+ restore();
+ }
+ }
+ },
+
+ font: function() {
+ var value = [], expression = [], weight, shorthand, font, e;
+
+ while (e = $(this.shorthand) || $(this.entity)) {
+ expression.push(e);
+ }
+ value.push(new tree.Expression(expression));
+
+ if ($(',')) {
+ while (e = $(this.expression)) {
+ value.push(e);
+ if (! $(',')) { break }
+ }
+ }
+ return new tree.Value(value);
+ },
+
+ //
+ // A Value is a comma-delimited list of Expressions
+ //
+ // font-family: Baskerville, Georgia, serif;
+ //
+ // In a Rule, a Value represents everything after the `:`,
+ // and before the `;`.
+ //
+ value: function() {
+ var e, expressions = [];
+
+ while (e = $(this.expression)) {
+ expressions.push(e);
+ if (! $(',')) { break }
+ }
+
+ if (expressions.length > 0) {
+ return new tree.Value(expressions);
+ }
+ },
+ sub: function() {
+ var e;
+
+ if ($('(') && (e = $(this.expression)) && $(')')) {
+ return e;
+ }
+ },
+ multiplication: function() {
+ var m, a, op, operation;
+ if (m = $(this.operand)) {
+ while ((op = ($('/') || $('*'))) && (a = $(this.operand))) {
+ operation = new tree.Operation(op, [operation || m, a], memo);
+ }
+ return operation || m;
+ }
+ },
+ addition: function() {
+ var m, a, op, operation;
+ if (m = $(this.multiplication)) {
+ while ((op = $(/^[-+]\s+/) || (input.charAt(i - 1) != ' ' && ($('+') || $('-')))) &&
+ (a = $(this.multiplication))) {
+ operation = new tree.Operation(op, [operation || m, a], memo);
+ }
+ 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.entities.dimension) ||
+ $(this.entities.color) || $(this.entities.variable) ||
+ $(this.entities.call);
+ },
+
+ //
+ // Expressions either represent mathematical operations,
+ // or white-space delimited Entities.
+ //
+ // 1px solid black
+ // @var * 2
+ //
+ expression: function() {
+ var e, delim, entities = [], d;
+
+ while (e = $(this.addition) || $(this.entity)) {
+ entities.push(e);
+ }
+ if (entities.length > 0) {
+ return new tree.Expression(entities);
+ }
+ },
+ property: function() {
+ var name;
+
+ if (name = $(/^(\*?-?[-a-z_0-9]+)\s*:/)) {
+ return name[1];
+ }
+ }
+ }
+ };
+};
+
+if (typeof(window) !== 'undefined') {
+ //
+ // Used by `@import` directives
+ //
+ carto.Parser.importer = function(path, paths, callback, env) {
+ if (path.charAt(0) !== '/' && paths.length > 0) {
+ path = paths[0] + path;
+ }
+ // We pass `true` as 3rd argument, to force the reload of the import.
+ // This is so we can get the syntax tree as opposed to just the CSS output,
+ // as we need this to evaluate the current stylesheet.
+ loadStyleSheet({ href: path, title: path, type: env.mime }, callback, true);
+ };
+}
diff --git a/lib/carto/renderer.js b/lib/carto/renderer.js
new file mode 100644
index 0000000..08fb4e5
--- /dev/null
+++ b/lib/carto/renderer.js
@@ -0,0 +1,479 @@
+var path = require('path'),
+ fs = require('fs'),
+ External = require('./external'),
+ Step = require('step'),
+ _ = require('underscore')._,
+ sys = require('sys'),
+ carto = require('carto'),
+ tree = require('carto/tree');
+
+require.paths.unshift(path.join(__dirname, '..', 'lib'));
+
+// Rendering circuitry for JSON map manifests.
+// This is node-only for the time being.
+carto.Renderer = function Renderer(env) {
+ env = _.extend({}, env);
+ if (!env.debug) env.debug = false;
+ if (!env.data_dir) env.data_dir = '/tmp/';
+ if (!env.local_data_dir) env.local_data_dir = '';
+ if (!env.validation_data) env.validation_data = false;
+
+ return {
+ // Keep a copy of passed-in environment variables
+ env: env,
+
+ // Ensure that map layers have a populated SRS value and attempt to
+ // autodetect SRS if missing. Requires that node-srs is available and
+ // that any remote datasources have been localized.
+ //
+ // - @param {Object} m map object.
+ // - @param {Function} callback
+ ensureSRS: function(m, callback) {
+ Step(
+ function autodetectSRS() {
+ var group = this.group();
+ var autodetect = _.filter(m.Layer, function(l) { return !l.srs; });
+ _.each(autodetect, function(l) {
+ var next = group();
+ Step(
+ function() {
+ fs.readdir(path.dirname(l.Datasource.file), this);
+ },
+ function(err, files) {
+ // Confirm that layers have .prj files
+ // in line with the .shp and .dbf, etc files.
+ var prj = _.detect(files, function(f) {
+ return path.extname(f).toLowerCase() == '.prj';
+ });
+ if (prj) {
+ prj = path.join(path.dirname(l.Datasource.file), prj);
+ fs.readFile(prj, 'utf-8', this);
+ } else {
+ this(new Error('No projection found'));
+ }
+ },
+ function(err, srs) {
+ if (!err) {
+ try {
+ l.srs = require('srs').parse(srs).proj4;
+ } catch (err) {}
+ }
+ next(err);
+ }
+ );
+ });
+ // If no layers missing SRS information, next.
+ autodetect.length === 0 && group()();
+ },
+ function waitForDetection(err) {
+ m.Layer = _.filter(m.Layer, function(l) { return l.srs; });
+ callback(err, m);
+ }
+ );
+ },
+
+ // Download any file-based remote datsources.
+ //
+ // Usable as an entry point: does not expect any modification to
+ // the map object beyond JSON parsing.
+ //
+ // - @param {Object} m map object.
+ // - @param {Function} callback
+ localizeExternals: function(m, callback) {
+ var that = this;
+ if (env.only_validate === true) {
+ callback(null, m);
+ return;
+ }
+ Step(
+ function downloadExternals() {
+ var group = this.group();
+ m.Layer.forEach(function(l) {
+ if (l.Datasource.file) {
+ var next = group();
+ new External(that.env, l.Datasource.file)
+ .on('err', function() {
+ next('Datasource could not be downloaded.');
+ })
+ .on('complete', function(external) {
+ external.findDataFile(function(err, file) {
+ if (err) {
+ next(err);
+ }
+ else if (file) {
+ l.Datasource.file = file;
+ next(null);
+ }
+ else {
+ next("No .shp file found in zipfile.");
+ }
+ });
+ });
+ } else {
+ throw 'Layer does not have a datasource';
+ }
+ });
+ // Continue even if we don't have any layers.
+ group()();
+ },
+ function waitForDownloads(err) {
+ callback(err, m);
+ }
+ );
+ },
+
+ // Download any remote stylesheets
+ //
+ // - @param {Object} m map object.
+ // - @param {Function} callback
+ localizeStyle: function(m, callback) {
+ var that = this;
+ Step(
+ function downloadStyles() {
+ var group = this.group();
+ m.Stylesheet.forEach(function(s, k) {
+ if (!s.id) {
+ var next = group();
+ // TODO: handle stylesheet externals
+ // that fail.
+ new External(that.env, s)
+ .on('complete', function(external) {
+ m.Stylesheet[k] = external.path();
+ next();
+ });
+ }
+ });
+ group()();
+ },
+ function waitForDownloads(err, results) {
+ callback(err, m);
+ }
+ );
+ },
+
+ // Compile (already downloaded) styles with carto,
+ // calling callback with an array of [map object, [stylesheet objects]]
+ //
+ // Called with the results of localizeStyle or localizeExternals:
+ // expects not to handle downloading.
+ //
+ // - @param {Object} m map object.
+ // - @param {Function} callback
+ style: function(m, callback) {
+ var that = this;
+ Step(
+ function loadStyles() {
+ var group = this.group();
+ m.Stylesheet.forEach(function(s) {
+ var next = group();
+ // If a stylesheet is an object with an id
+ // property, it will have a .data property
+ // as well which can be parsed directly
+ if (s.id) {
+ next(null, [s.id, s.data]);
+ // Otherwise the value is assumed to be a
+ // string and is read from the filesystem
+ } else {
+ fs.readFile(s, 'utf-8', function(err, data) {
+ next(err, [s, data]);
+ });
+ }
+ });
+ },
+ function compileStyles(e, results) {
+ var options = {},
+ group = this.group();
+ results.forEach(function(result) {
+ var next = group();
+ var parsingTime = +new Date;
+ // Maintain a parsing environment from
+ // stylesheet to stylesheet, so that frames
+ // and effects are maintained.
+ var parse_env = _.extend(
+ _.extend(
+ that.env,
+ this.env
+ ), { filename: result[0] });
+ new carto.Parser(parse_env).parse(result[1],
+ function(err, tree) {
+ if (env.debug) console.warn('Parsing time: '
+ + ((new Date - parsingTime))
+ + 'ms (' + result[0] + ')');
+ try {
+ next(err, [
+ result[0],
+ tree]);
+ return;
+ } catch (e) {
+ throw e;
+ return;
+ }
+ });
+ });
+ },
+ function waitForCompilation(err, res) {
+ callback(err, m, res);
+ }
+ );
+ },
+
+ addRules: function(current, definition, existing) {
+ var newFilters = definition.filters;
+ var newRules = definition.rules;
+ var updatedFilters, clone, previous;
+
+ // The current definition might have been split up into
+ // multiple definitions already.
+ for (var k = 0; k < current.length; k++) {
+ updatedFilters = current[k].filters.cloneWith(newFilters);
+ if (updatedFilters) {
+ previous = existing[updatedFilters];
+ if (previous) {
+ // There's already a definition with those exact
+ // filters. Add the current definitions' rules
+ // and stop processing it as the existing rule
+ // has already gone down the inheritance chain.
+ previous.addRules(newRules);
+ } else {
+ clone = current[k].clone(updatedFilters);
+ // Make sure that we're only maintaining the clone
+ // when we did actually add rules. If not, there's
+ // no need to keep the clone around.
+ if (clone.addRules(newRules)) {
+ // We inserted an element before this one, so we need
+ // to make sure that in the next loop iteration, we're
+ // not performing the same task for this element again,
+ // hence the k++.
+ current.splice(k, 0, clone);
+ k++;
+ }
+ }
+ } else if (updatedFilters === null) {
+ // Filters can be added, but they don't change the
+ // filters. This means we don't have to split the
+ // definition.
+ current[k].addRules(newRules);
+ }
+ }
+ return current;
+ },
+
+ /**
+ * Apply inherited styles from their
+ * ancestors to them.
+ */
+ inheritRules: function(definitions) {
+ var inheritTime = +new Date();
+ // definitions are ordered by specificity,
+ // high (index 0) to low
+ var byAttachment = {}, byFilter = {};
+ var result = [];
+ var current, previous, attachment;
+
+ for (var i = 0; i < definitions.length; i++) {
+ attachment = definitions[i].attachment;
+ if (!byAttachment[attachment]) {
+ byAttachment[attachment] = [];
+ byAttachment[attachment].attachment = attachment;
+ byFilter[attachment] = {};
+ result.push(byAttachment[attachment]);
+ }
+
+ current = [ definitions[i] ];
+ // Iterate over all subsequent rules.
+ for (var j = i + 1; j < definitions.length; j++) {
+ if (definitions[j].attachment === attachment) {
+ // Only inherit rules from the same attachment.
+ current = this.addRules(current, definitions[j], byFilter);
+ }
+ }
+
+ for (var j = 0; j < current.length; j++) {
+ byFilter[attachment][current[j].filters] = current[j];
+ byAttachment[attachment].push(current[j]);
+ }
+ }
+
+ if (env.debug) console.warn('Inheritance time: ' + ((new Date - inheritTime)) + 'ms');
+
+ return result;
+ },
+
+ // Find a rule like Map { background-color: #fff; },
+ // if any, and return a list of properties to be inserted
+ // into the <Map element of the resulting XML.
+ //
+ // - @param {Array} rulesets the output of toList.
+ // - @param {Object} env.
+ // - @return {String} rendered properties.
+ getMapProperties: function(rulesets, env) {
+ var properties = [];
+ var rules_added = {};
+ rulesets.filter(function(r) {
+ return r.elements.join('') === 'Map';
+ }).forEach(function(r) {
+ for (var i = 0; i < r.rules.length; i++) {
+ if (!rules_added[r.rules[i].name]) {
+ properties.push(r.rules[i].eval(env).toXML(env));
+ rules_added[r.rules[i].name] = true;
+ }
+ }
+ });
+ return properties.join(' ');
+ },
+
+ // Prepare full XML map output. Called with the results
+ // of this.style
+ //
+ // - @param {Array} res array of [map object, stylesheets].
+ // - @param {Function} callback
+ template: function(err, m, stylesheets, callback) {
+ // frames is a container for variables and other less.js
+ // constructs.
+ //
+ // effects is a container for side-effects, which currently
+ // are limited to FontSets.
+ //
+ // deferred_externals is a list of externals, like
+ // pattern images, etc (by string URL)
+ // referred to by url() constructs in styles
+ var that = this,
+ env = _.extend({
+ frames: tree.Reference.color_frames(),
+ effects: [],
+ deferred_externals: []
+ }, that.env),
+ output = [];
+
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ var rulesets = _.flatten(stylesheets.map(function(rulesets) {
+ return rulesets[1].toList(env);
+ }));
+
+ // Iterate through layers and create styles custom-built
+ // for each of them, and apply those styles to the layers.
+ m.Layer.forEach(function(l) {
+ l.styles = [];
+ // Classes are given as space-separated alphanumeric
+ // strings.
+ var classes = (l['class'] || '').split(/\s+/g);
+
+ var matching = rulesets.filter(function(definition) {
+ return definition.appliesTo(l.id, classes);
+ });
+
+ if (!l.id) {
+ l.id = 'layer-' + (Math.random() * 1e16).toFixed();
+ }
+ if (!l.name) {
+ l.name = l.id;
+ }
+
+ var definitions = that.inheritRules(matching);
+
+ for (var i = 0; i < definitions.length; i++) {
+ var style = new tree.Style(l.id, definitions[i].attachment, definitions[i]);
+ if (style) {
+ l.styles.push(style.name);
+
+ // env.effects can be modified by this call
+ output.push(style.toXML(env));
+ }
+ }
+
+ var nl = new carto.tree.Layer(l);
+ output.push(nl.toXML());
+ });
+ output.unshift(env.effects.map(function(e) {
+ return e.toXML(env);
+ }).join('\n'));
+
+ Step(
+ function() {
+ var group = this.group();
+ var map_properties = that.getMapProperties(rulesets, env);
+ // The only_validate flag can be set to prevent
+ // any externals from being downloaded - for instance,
+ // when only validation is needed.
+ if (env.only_validate !== true &&
+ env.deferred_externals.length) {
+ env.deferred_externals.forEach(function(def) {
+ var next = group();
+ new External(that.env, def)
+ .on('complete', function(external) {
+ next(null, map_properties);
+ });
+ });
+ } else {
+ var next = group();
+ next(null, map_properties);
+ }
+ },
+ function(err, map_properties) {
+ output.unshift(
+ '<?xml version="1.0" '
+ + 'encoding="utf-8"?>\n'
+ + '<!DOCTYPE Map[]>\n'
+ + '<Map '
+ + map_properties
+ + ' srs="+proj=merc +a=6378137 +b=6378137 '
+ + '+lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m '
+ + '+nadgrids=@null +no_defs">\n');
+
+ output.push('</Map>');
+
+ env.errors.map(function(e) {
+ if (!e.line && e.index && e.filename) {
+ var matches = stylesheets.filter(function(s) {
+ return s[0] == e.filename;
+ });
+ if (matches.length && matches[0][1]) {
+ return _.extend(e, matches[0][1].makeError(e));
+ }
+ }
+ });
+
+ callback(env.errors.length ?
+ env.errors :
+ null,
+ output.join('\n'));
+ }
+ );
+ },
+
+ // Prepare a MML document (given as a string) into a
+ // fully-localized XML file ready for Mapnik2 consumption
+ //
+ // - @param {String} str the JSON file as a string.
+ // - @param {Function} callback to be called with err,
+ // XML representation.
+ render: function(str, callback) {
+ var m = (typeof str == "string") ? JSON.parse(str) : str,
+ that = this;
+
+ var localizingTime = +new Date;
+ this.localizeExternals(m, function(err, res) {
+ that.ensureSRS(res, function(err, res) {
+ that.localizeStyle(res, function(err, res) {
+ if (env.debug) console.warn('Localizing time: '
+ + ((new Date - localizingTime)) + 'ms');
+ that.style(res, function(err, m, res) {
+ var compilingTime = +new Date;
+ that.template(err, m, res, function(err, res) {
+ if (env.debug) console.warn('COMPILING TIME: '
+ + ((new Date - compilingTime)) + 'ms');
+ callback(err, res);
+ });
+ });
+ });
+ });
+ });
+ }
+ };
+};
+
+module.exports = carto;
diff --git a/lib/carto/tree.js b/lib/carto/tree.js
new file mode 100644
index 0000000..f93e3c0
--- /dev/null
+++ b/lib/carto/tree.js
@@ -0,0 +1,9 @@
+/**
+ * TODO: document this. What does this do?
+ */
+require('carto/tree').find = function (obj, fun) {
+ for (var i = 0, r; i < obj.length; i++) {
+ if (r = fun.call(obj, obj[i])) { return r }
+ }
+ return null;
+};
diff --git a/lib/carto/tree/alpha.js b/lib/carto/tree/alpha.js
new file mode 100644
index 0000000..c925ad4
--- /dev/null
+++ b/lib/carto/tree/alpha.js
@@ -0,0 +1,14 @@
+(function(tree) {
+
+tree.Alpha = function Alpha(val) {
+ this.value = val;
+};
+tree.Alpha.prototype = {
+ toString: function() {
+ return 'alpha(opacity=' +
+ (this.value.toString ? this.value.toString() : this.value) + ')';
+ },
+ eval: function() { return this }
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/anonymous.js b/lib/carto/tree/anonymous.js
new file mode 100644
index 0000000..97ec471
--- /dev/null
+++ b/lib/carto/tree/anonymous.js
@@ -0,0 +1,13 @@
+(function(tree) {
+
+tree.Anonymous = function Anonymous(string) {
+ this.value = string.value || string;
+};
+tree.Anonymous.prototype = {
+ toString: function() {
+ return this.value;
+ },
+ eval: function() { return this }
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/call.js b/lib/carto/tree/call.js
new file mode 100644
index 0000000..6ad6de2
--- /dev/null
+++ b/lib/carto/tree/call.js
@@ -0,0 +1,39 @@
+(function(tree) {
+
+//
+// A function call node.
+//
+tree.Call = function Call(name, args) {
+ this.name = name;
+ this.args = args;
+};
+tree.Call.prototype = {
+ //
+ // When evaluating a function call,
+ // we either find the function in `tree.functions` [1],
+ // in which case we call it, passing the evaluated arguments,
+ // or we simply print it out as it appeared originally [2].
+ //
+ // The *functions.js* file contains the built-in functions.
+ //
+ // The reason why we evaluate the arguments, is in the case where
+ // we try to pass a variable to a function, like: `saturate(@color)`.
+ // The function should receive the value, not the variable.
+ //
+ eval: function(env) {
+ var args = this.args.map(function(a) { return a.eval(env) });
+
+ if (this.name in tree.functions) { // 1.
+ return tree.functions[this.name].apply(tree.functions, args);
+ } else { // 2.
+ return new tree.Anonymous(this.name +
+ '(' + args.map(function(a) { return a.toString() }).join(', ') + ')');
+ }
+ },
+
+ toString: function(env) {
+ return this.eval(env).toString();
+ }
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/color.js b/lib/carto/tree/color.js
new file mode 100644
index 0000000..3c5b0c7
--- /dev/null
+++ b/lib/carto/tree/color.js
@@ -0,0 +1,94 @@
+(function(tree) {
+//
+// RGB Colors - #ff0014, #eee
+//
+tree.Color = function Color(rgb, a) {
+ //
+ // The end goal here, is to parse the arguments
+ // into an integer triplet, such as `128, 255, 0`
+ //
+ // This facilitates operations and conversions.
+ //
+ if (Array.isArray(rgb)) {
+ this.rgb = rgb;
+ } else if (rgb.length == 6) {
+ this.rgb = rgb.match(/.{2}/g).map(function(c) {
+ return parseInt(c, 16);
+ });
+ } else {
+ this.rgb = rgb.split('').map(function(c) {
+ return parseInt(c + c, 16);
+ });
+ }
+ this.is = 'color';
+ this.alpha = typeof(a) === 'number' ? a : 1;
+};
+tree.Color.prototype = {
+ eval: function() { return this },
+
+ //
+ // If we have some transparency, the only way to represent it
+ // is via `rgba`. Otherwise, we use the hex representation,
+ // which has better compatibility with older browsers.
+ // Values are capped between `0` and `255`, rounded and zero-padded.
+ //
+ toString: function() {
+ if (this.alpha < 1.0) {
+ return 'rgba(' + this.rgb.map(function(c) {
+ return Math.round(c);
+ }).concat(this.alpha).join(', ') + ')';
+ } else {
+ return '#' + this.rgb.map(function(i) {
+ i = Math.round(i);
+ i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16);
+ return i.length === 1 ? '0' + i : i;
+ }).join('');
+ }
+ },
+
+ //
+ // Operations have to be done per-channel, if not,
+ // channels will spill onto each other. Once we have
+ // our result, in the form of an integer triplet,
+ // we create a new Color node to hold the result.
+ //
+ operate: function(op, other) {
+ var result = [];
+
+ if (! (other instanceof tree.Color)) {
+ other = other.toColor();
+ }
+
+ for (var c = 0; c < 3; c++) {
+ result[c] = tree.operate(op, this.rgb[c], other.rgb[c]);
+ }
+ return new tree.Color(result);
+ },
+
+ toHSL: function() {
+ var r = this.rgb[0] / 255,
+ g = this.rgb[1] / 255,
+ b = this.rgb[2] / 255,
+ a = this.alpha;
+
+ var max = Math.max(r, g, b), min = Math.min(r, g, b);
+ var h, s, l = (max + min) / 2, d = max - min;
+
+ if (max === min) {
+ h = s = 0;
+ } else {
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+
+ switch (max) {
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: h = (b - r) / d + 2; break;
+ case b: h = (r - g) / d + 4; break;
+ }
+ h /= 6;
+ }
+ return { h: h * 360, s: s, l: l, a: a };
+ }
+};
+
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/comment.js b/lib/carto/tree/comment.js
new file mode 100644
index 0000000..49cb4fa
--- /dev/null
+++ b/lib/carto/tree/comment.js
@@ -0,0 +1,14 @@
+(function(tree) {
+
+tree.Comment = function Comment(value, silent) {
+ this.value = value;
+ this.silent = !!silent;
+};
+tree.Comment.prototype = {
+ toString: function(env) {
+ return '<!--' + this.value + '-->';
+ },
+ eval: function() { return this }
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/definition.js b/lib/carto/tree/definition.js
new file mode 100644
index 0000000..b4f14cc
--- /dev/null
+++ b/lib/carto/tree/definition.js
@@ -0,0 +1,157 @@
+(function(tree) {
+var assert = require('assert');
+
+tree.Definition = function Definition(selector, rules) {
+ this.elements = selector.elements;
+ assert.ok(selector.filters instanceof tree.Filterset);
+ this.rules = rules;
+ this.ruleIndex = [];
+ for (var i = 0; i < this.rules.length; i++) {
+ if ('zoom' in this.rules[i]) this.rules[i] = this.rules[i].clone();
+ this.rules[i].zoom = selector.zoom;
+ this.ruleIndex.push(this.rules[i].updateID());
+ }
+ this.filters = selector.filters;
+ this.zoom = selector.zoom;
+ this.attachment = selector.attachment || '__default__';
+ this.specificity = selector.specificity();
+};
+
+tree.Definition.prototype.toString = function() {
+ var str = this.filters.toString();
+ for (var i = 0; i < this.rules.length; i++) {
+ str += '\n ' + this.rules[i];
+ }
+ return str;
+};
+
+tree.Definition.prototype.clone = function(filters) {
+ assert.ok(filters instanceof tree.Filterset);
+ var clone = Object.create(tree.Definition.prototype);
+ clone.rules = this.rules.slice();
+ clone.ruleIndex = this.ruleIndex.slice();
+ clone.filters = filters;
+ clone.attachment = this.attachment;
+ return clone;
+};
+
+tree.Definition.prototype.addRules = function(rules) {
+ var added = 0;
+
+ // Add only unique rules.
+ for (var i = 0; i < rules.length; i++) {
+ if (this.ruleIndex.indexOf(rules[i].id) < 0) {
+ this.rules.push(rules[i]);
+ this.ruleIndex.push(rules[i].id);
+ added++;
+ }
+ }
+
+ return added;
+};
+
+/**
+ * Determine whether this selector matches a given id
+ * and array of classes, by determining whether
+ * all elements it contains match.
+ */
+tree.Definition.prototype.appliesTo = function(id, classes) {
+ for (var i = 0; i < this.elements.length; i++) {
+ if (!this.elements[i].matches(id, classes)) {
+ return false;
+ }
+ }
+ return true;
+};
+
+tree.Definition.prototype.hasSymbolizer = function(symbolizer) {
+ for (var i = 0; i < this.rules.length; i++) {
+ if (this.rules[i].symbolizer === symbolizer) {
+ return true;
+ }
+ }
+ return false;
+};
+
+tree.Definition.prototype.symbolizersToXML = function(env, symbolizers, zoom) {
+ var xml = ' <Rule>\n';
+ xml += tree.Zoom.toXML(zoom).join('');
+ xml += this.filters.toXML(env);
+
+ for (var symbolizer in symbolizers) {
+ attributes = symbolizers[symbolizer];
+ if (fail = tree.Reference.requiredProperties(symbolizer, attributes)) {
+ var rule = attributes[Object.keys(attributes).shift()];
+ env.error({
+ message: fail,
+ index: rule.index,
+ filename: rule.filename
+ });
+ }
+
+ name = symbolizer.charAt(0).toUpperCase() +
+ symbolizer.slice(1).replace(/\-./, function(str) {
+ return str[1].toUpperCase();
+ }) + 'Symbolizer';
+
+ xml += ' <' + name + ' ';
+ for (var key in attributes) {
+ xml += attributes[key].eval(env).toXML(env) + ' ';
+ }
+ xml += '/>\n';
+ }
+ xml += ' </Rule>\n';
+ return xml;
+};
+
+tree.Definition.prototype.collectSymbolizers = function(zooms, i) {
+ var symbolizers = {}, child;
+
+ for (var j = i; j < this.rules.length; j++) {
+ child = this.rules[j];
+ if (zooms.current & child.zoom &&
+ (!(child.symbolizer in symbolizers) ||
+ (!(child.name in symbolizers[child.symbolizer])))) {
+ zooms.current &= child.zoom;
+ if (!(child.symbolizer in symbolizers)) {
+ symbolizers[child.symbolizer] = {};
+ }
+ symbolizers[child.symbolizer][child.name] = child;
+ }
+ }
+
+ if (Object.keys(symbolizers).length) {
+ zooms.rule &= (zooms.available &= ~zooms.current);
+ return symbolizers;
+ }
+};
+
+tree.Definition.prototype.toXML = function(env, existing) {
+ // The tree.Zoom.toString function ignores the holes in zoom ranges and outputs
+ // scaledenominators that cover the whole range from the first to last bit set.
+ // This algorithm can produces zoom ranges that may have holes. However,
+ // when using the filter-mode="first", more specific zoom filters will always
+ // end up before broader ranges. The filter-mode will pick those first before
+ // resorting to the zoom range with the hole and stop processing further rules.
+ var filter = this.filters.toString();
+ if (!(filter in existing)) existing[filter] = tree.Zoom.all;
+
+ var available = tree.Zoom.all, xml = '', zoom, symbolizers;
+ var zooms = { available: tree.Zoom.all };
+ for (var i = 0; i < this.rules.length && available; i++) {
+ zooms.rule = this.rules[i].zoom;
+ if (!(existing[filter] & zooms.rule)) continue;
+
+ while (zooms.current = zooms.rule & available) {
+ if (symbolizers = this.collectSymbolizers(zooms, i)) {
+ if (!(existing[filter] & zooms.current)) continue;
+ xml += this.symbolizersToXML(env, symbolizers, existing[filter] & zooms.current);
+ existing[filter] &= ~zooms.current;
+ }
+ }
+ }
+
+ return xml;
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/dimension.js b/lib/carto/tree/dimension.js
new file mode 100644
index 0000000..9fa774f
--- /dev/null
+++ b/lib/carto/tree/dimension.js
@@ -0,0 +1,44 @@
+(function(tree) {
+
+//
+// A number with a unit
+//
+tree.Dimension = function Dimension(value, unit, index) {
+ this.value = parseFloat(value);
+ this.unit = unit || null;
+ this.is = 'float';
+ this.index = index;
+};
+
+tree.Dimension.prototype = {
+ eval: function () {
+ if (this.unit && this.unit !== 'px') {
+ throw {
+ message: "Invalid unit: '" + this.unit + "'",
+ index: this.index
+ };
+ }
+
+ return this;
+ },
+ toColor: function() {
+ return new tree.Color([this.value, this.value, this.value]);
+ },
+ toString: function() {
+ return this.value;
+ },
+
+ // In an operation between two Dimensions,
+ // we default to the first Dimension's unit,
+ // so `1px + 2em` will yield `3px`.
+ // In the future, we could implement some unit
+ // conversions such that `100cm + 10mm` would yield
+ // `101cm`.
+ operate: function(op, other) {
+ return new tree.Dimension;
+ (tree.operate(op, this.value, other.value),
+ this.unit || other.unit);
+ }
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/directive.js b/lib/carto/tree/directive.js
new file mode 100644
index 0000000..9248dfc
--- /dev/null
+++ b/lib/carto/tree/directive.js
@@ -0,0 +1,33 @@
+(function(tree) {
+
+tree.Directive = function Directive(name, value) {
+ this.name = name;
+ if (Array.isArray(value)) {
+ this.ruleset = new tree.Ruleset([], value);
+ } else {
+ this.value = value;
+ }
+};
+tree.Directive.prototype = {
+ toString: function(ctx, env) {
+ if (this.ruleset) {
+ this.ruleset.root = true;
+ return this.name + ' {\n ' +
+ this.ruleset.toString(ctx, env).trim().replace(/\n/g, '\n ') +
+ '\n}\n';
+ } else {
+ return this.name + ' ' + this.value.toString() + ';\n';
+ }
+ },
+ eval: function(env) {
+ env.frames.unshift(this);
+ this.ruleset = this.ruleset && this.ruleset.eval(env);
+ env.frames.shift();
+ return this;
+ },
+ variable: function(name) { return tree.Ruleset.prototype.variable.call(this.ruleset, name) },
+ find: function() { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) },
+ rulesets: function() { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) }
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/element.js b/lib/carto/tree/element.js
new file mode 100644
index 0000000..8993c18
--- /dev/null
+++ b/lib/carto/tree/element.js
@@ -0,0 +1,32 @@
+(function(tree) {
+
+// An element is an id or class selector
+tree.Element = function Element(value) {
+ this.value = value.trim();
+};
+
+// Determine the 'specificity matrix' of this
+// specific selector
+tree.Element.prototype.specificity = function() {
+ return [
+ (this.value[0] == '#') ? 1 : 0, // a
+ (this.value[0] == '.') ? 1 : 0 // b
+ ];
+};
+
+tree.Element.prototype.toString = function() {
+ return this.value;
+};
+
+// Determine whether this element matches an id or classes.
+// An element is a single id or class, or check whether the given
+// array of classes contains this, or the id is equal to this.
+//
+// Takes a plain string for id and plain strings in the array of
+// classes.
+tree.Element.prototype.matches = function(id, classes) {
+ return (classes.indexOf(this.value.replace(/^\./, '')) !== -1) ||
+ (this.value.replace(/^#/, '') == id);
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/expression.js b/lib/carto/tree/expression.js
new file mode 100644
index 0000000..9ee5736
--- /dev/null
+++ b/lib/carto/tree/expression.js
@@ -0,0 +1,21 @@
+(function(tree) {
+
+tree.Expression = function Expression(value) { this.value = value };
+tree.Expression.prototype = {
+ eval: function(env) {
+ if (this.value.length > 1) {
+ return new tree.Expression(this.value.map(function(e) {
+ return e.eval(env);
+ }));
+ } else {
+ return this.value[0].eval(env);
+ }
+ },
+ toString: function(env) {
+ return this.value.map(function(e) {
+ return e.toString(env);
+ }).join(' ');
+ }
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/filter.js b/lib/carto/tree/filter.js
new file mode 100644
index 0000000..6ee29b8
--- /dev/null
+++ b/lib/carto/tree/filter.js
@@ -0,0 +1,54 @@
+(function(tree) {
+
+tree.Filter = function Filter(key, op, val, index) {
+ if (key.is) {
+ this.key = key.value;
+ this._key = key;
+ } else {
+ this.key = key;
+ }
+
+ this.op = op;
+
+ if (val.is) {
+ this.val = val.value;
+ this._val = val;
+ } else {
+ this.val = val;
+ }
+
+ if (op !== '=' && op !== '!=') {
+ this.val = 1*this.val;
+ if (isNaN(this.val)) {
+ throw {
+ message: 'Cannot use operator "' + op + '" with value ' + val,
+ index: index
+ };
+ }
+ }
+ this.id = this.key + this.op + this.val;
+};
+
+
+// XML-safe versions of comparators
+var opXML = {
+ '<': '&lt;',
+ '>': '&gt;',
+ '=': '=',
+ '!=': '!=',
+ '<=': '&lt;=',
+ '>=': '&gt;='
+};
+
+tree.Filter.prototype.toXML = function(env) {
+ if (this._key) var key = this._key.toString(this._key.is == 'string');
+ if (this._val) var val = this._val.toString(this._val.is == 'string');
+
+ return '[' + (key || this.key) + '] ' + opXML[this.op] + ' ' + (val || this.val);
+};
+
+tree.Filter.prototype.toString = function() {
+ return '[' + this.id + ']';
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/filterset.js b/lib/carto/tree/filterset.js
new file mode 100644
index 0000000..6b6d38e
--- /dev/null
+++ b/lib/carto/tree/filterset.js
@@ -0,0 +1,196 @@
+var tree = require('carto/tree');
+
+tree.Filterset = function Filterset() {};
+
+Object.defineProperty(tree.Filterset.prototype, 'toXML', {
+ enumerable: false,
+ value: function(env) {
+ var filters = [];
+ for (var id in this) {
+ filters.push('(' + this[id].toXML(env).trim() + ')');
+ }
+
+ if (filters.length) {
+ return ' <Filter>' + filters.join(' and ') + '</Filter>\n';
+ } else {
+ return '';
+ }
+ }
+});
+
+Object.defineProperty(tree.Filterset.prototype, 'toString', {
+ enumerable: false,
+ value: function() {
+ var arr = [];
+ for (var id in this) arr.push(this[id].id);
+ arr.sort();
+ return arr.join('\t');
+ }
+});
+
+// Note: other has to be a tree.Filterset.
+Object.defineProperty(tree.Filterset.prototype, 'cloneWith', {
+ enumerable: false,
+ value: function(other) {
+ var additions;
+ for (var id in other) {
+ var status = this.addable(other[id]);
+ if (status === false) {
+ return false;
+ }
+ if (status === true) {
+ // Adding the filter will override another value.
+ if (!additions) additions = [];
+ additions.push(other[id]);
+ }
+ }
+
+ // Adding the other filters doesn't make this filterset invalid, but it
+ // doesn't add anything to it either.
+ if (!additions) return null;
+
+ // We can successfully add all filters. Now clone the filterset and add the
+ // new rules.
+ var clone = new tree.Filterset();
+
+ // We can add the rules that are already present without going through the
+ // add function as a Filterset is always in it's simplest canonical form.
+ for (var id in this)
+ clone[id] = this[id];
+
+ // Only add new filters that actually change the filter.
+ while (id = additions.shift())
+ clone.add(id);
+
+ return clone;
+ }
+});
+
+/**
+ * Returns true when the new filter can be added, false otherwise.
+ */
+Object.defineProperty(tree.Filterset.prototype, 'addable', {
+ enumerable: false,
+ value: function(filter) {
+ var key = filter.key, value = filter.val;
+
+ switch (filter.op) {
+ case '=':
+ if (key + '=' in this) return (this[key + '='].val != value) ? false : null;
+ if (key + '!=' + value in this) return false;
+ if (key + '>' in this && this[key + '>'].val >= value) return false;
+ if (key + '<' in this && this[key + '<'].val <= value) return false;
+ if (key + '>=' in this && this[key + '>='].val > value) return false;
+ if (key + '<=' in this && this[key + '<='].val < value) return false;
+ return true;
+
+ case '!=':
+ if (key + '=' in this) return (this[key + '='].val == value) ? false : null;
+ if (key + '!=' + value in this) return null;
+
+ if (key + '>' in this && this[key + '>'].val >= value) return null;
+ if (key + '<' in this && this[key + '<'].val <= value) return null;
+ if (key + '>=' in this && this[key + '>='].val > value) return null;
+ if (key + '<=' in this && this[key + '<='].val < value) return null;
+
+ return true;
+
+ case '>':
+ if (key + '=' in this) return (this[key + '='].val <= value) ? false : null;
+ if (key + '<' in this && this[key + '<'].val <= value) return false;
+ if (key + '<=' in this && this[key + '<='].val <= value) return false;
+ if (key + '>' in this && this[key + '>'].val >= value) return null;
+ if (key + '>=' in this && this[key + '>='].val > value) return null;
+ return true;
+
+ case '>=':
+ if (key + '=' in this) return (this[key + '='].val < value) ? false : null;
+ if (key + '<' in this && this[key + '<'].val <= value) return false;
+ if (key + '<=' in this && this[key + '<='].val < value) return false;
+ if (key + '>' in this && this[key + '>'].val >= value) return null;
+ if (key + '>=' in this && this[key + '>='].val >= value) return null;
+ return true;
+
+ case '<':
+ if (key + '=' in this) return (this[key + '='].val >= value) ? false : null;
+ if (key + '>' in this && this[key + '>'].val >= value) return false;
+ if (key + '>=' in this && this[key + '>='].val >= value) return false;
+ if (key + '<' in this && this[key + '<'].val <= value) return null;
+ if (key + '<=' in this && this[key + '<='].val < value) return null;
+ return true;
+
+ case '<=':
+ if (key + '=' in this) return (this[key + '='].val > value) ? false : null;
+ if (key + '>' in this && this[key + '>'].val >= value) return false;
+ if (key + '>=' in this && this[key + '>='].val > value) return false;
+ if (key + '<' in this && this[key + '<'].val <= value) return null;
+ if (key + '<=' in this && this[key + '<='].val <= value) return null;
+ return true;
+ }
+ }
+});
+
+/**
+ * Only call this function for filters that have been cleared by .addable().
+ */
+Object.defineProperty(tree.Filterset.prototype, 'add', {
+ enumerable: false,
+ value: function(filter) {
+ var key = filter.key;
+
+ switch (filter.op) {
+ case '=':
+ for (var id in this)
+ if (this[id].key == key)
+ delete this[id];
+ this[key + '='] = filter;
+ break;
+
+ case '!=':
+ this[key + '!=' + filter.val] = filter;
+ break;
+
+ case '>':
+ for (var id in this)
+ if (this[id].key == key && this[id].val <= filter.val)
+ delete this[id];
+ this[key + '>'] = filter;
+ break;
+
+ case '>=':
+ for (var id in this)
+ if (this[id].key == key && this[id].val < filter.val)
+ delete this[id];
+ if (key + '!=' + filter.val in this) {
+ delete this[key + '!=' + filter.val];
+ filter.op = '>';
+ this[key + '>'] = filter;
+ }
+ else {
+ this[key + '>='] = filter;
+ }
+ break;
+
+ case '<':
+ for (var id in this)
+ if (this[id].key == key && this[id].val >= filter.val)
+ delete this[id];
+ this[key + '<'] = filter;
+ break;
+
+ case '<=':
+ for (var id in this)
+ if (this[id].key == key && this[id].val > filter.val)
+ delete this[id];
+ if (key + '!=' + filter.val in this) {
+ delete this[key + '!=' + filter.val];
+ filter.op = '<';
+ this[key + '<'] = filter;
+ }
+ else {
+ this[key + '<='] = filter;
+ }
+ break;
+ }
+ }
+});
diff --git a/lib/carto/tree/fontset.js b/lib/carto/tree/fontset.js
new file mode 100644
index 0000000..dc6fa48
--- /dev/null
+++ b/lib/carto/tree/fontset.js
@@ -0,0 +1,38 @@
+(function(tree) {
+
+tree._getFontSet = function(env, fonts) {
+ var find_existing = function(fonts) {
+ var findFonts = fonts.join('');
+ for (var i = 0; i < env.effects.length; i++) {
+ if (findFonts == env.effects[0].fonts.join('')) {
+ return env.effects[0];
+ }
+ }
+ };
+
+ var existing = false;
+ if (existing = find_existing(fonts)) {
+ return existing;
+ } else {
+ var new_fontset = new tree.FontSet(env, fonts);
+ env.effects.push(new_fontset);
+ return new_fontset;
+ }
+};
+
+tree.FontSet = function FontSet(env, fonts) {
+ this.fonts = fonts;
+ this.name = 'fontset-' + env.effects.length;
+};
+
+tree.FontSet.prototype.toXML = function(env) {
+ return '<FontSet name="'
+ + this.name
+ + '">\n'
+ + this.fonts.map(function(f) {
+ return ' <Font face-name="' + f +'"/>';
+ }).join('\n')
+ + '\n</FontSet>'
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/import.js b/lib/carto/tree/import.js
new file mode 100644
index 0000000..8883ab0
--- /dev/null
+++ b/lib/carto/tree/import.js
@@ -0,0 +1,77 @@
+(function(tree) {
+//
+// CSS @import node
+//
+// The general strategy here is that we don't want to wait
+// for the parsing to be completed, before we start importing
+// the file. That's because in the context of a browser,
+// most of the time will be spent waiting for the server to respond.
+//
+// On creation, we push the import path to our import queue, though
+// `import,push`, we also pass it a callback, which it'll call once
+// the file has been fetched, and parsed.
+//
+tree.Import = function Import(path, imports) {
+ var that = this;
+
+ this._path = path;
+
+ // The '.mess' extension is optional
+ if (path instanceof tree.Quoted) {
+ this.path = /\.(le?|c)ss$/.test(path.value) ? path.value : path.value + '.mess';
+ } else {
+ this.path = path.value.value || path.value;
+ }
+
+ this.css = /css$/.test(this.path);
+
+ // Only pre-compile .mess files
+ if (! this.css) {
+ imports.push(this.path, function(root) {
+ if (! root) {
+ throw new Error('Error parsing ' + that.path);
+ }
+ that.root = root;
+ });
+ }
+};
+
+//
+// The actual import node doesn't return anything, when converted to CSS.
+// The reason is that it's used at the evaluation stage, so that the rules
+// it imports can be treated like any other rules.
+//
+// In `eval`, we make sure all Import nodes get evaluated, recursively, so
+// we end up with a flat structure, which can easily be imported in the parent
+// ruleset.
+//
+tree.Import.prototype = {
+ toString: function() {
+ if (this.css) {
+ return '@import ' + this._path.toString() + ';\n';
+ } else {
+ return '';
+ }
+ },
+ eval: function(env) {
+ var ruleset;
+
+ if (this.css) {
+ return this;
+ } else {
+ ruleset = new tree.Ruleset(null, this.root.rules.slice(0));
+
+ for (var i = 0; i < ruleset.rules.length; i++) {
+ if (ruleset.rules[i] instanceof tree.Import) {
+ Array.prototype
+ .splice
+ .apply(ruleset.rules,
+ [i, 1].concat(ruleset.rules[i].eval(env)));
+ }
+ }
+ return ruleset.rules;
+ }
+ }
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/invalid.js b/lib/carto/tree/invalid.js
new file mode 100644
index 0000000..5a49d96
--- /dev/null
+++ b/lib/carto/tree/invalid.js
@@ -0,0 +1,8 @@
+(function (tree) {
+tree.Invalid = function Invalid(chunk, index, message) {
+ this.chunk = chunk;
+ this.index = index;
+ this.type = 'syntax';
+ this.message = message || "Invalid code: " + this.chunk;
+};
+})(require('carto/tree'));
diff --git a/lib/carto/tree/javascript.js b/lib/carto/tree/javascript.js
new file mode 100644
index 0000000..4737a95
--- /dev/null
+++ b/lib/carto/tree/javascript.js
@@ -0,0 +1,38 @@
+(function(tree) {
+
+tree.JavaScript = function JavaScript(string, index) {
+ this.expression = string;
+ this.index = index;
+};
+tree.JavaScript.prototype = {
+ toString: function() {
+ return JSON.stringify(this.evaluated);
+ },
+ eval: function(env) {
+ var result,
+ expression = new Function('return (' + this.expression + ')'),
+ context = {};
+
+ for (var k in env.frames[0].variables()) {
+ context[k.slice(1)] = {
+ value: env.frames[0].variables()[k].value,
+ toJS: function() {
+ return this.value.eval(env).toString();
+ }
+ };
+ }
+
+ try {
+ this.evaluated = expression.call(context);
+ } catch (e) {
+ throw {
+ message: "JavaScript evaluation error: '" + e.name + ': ' + e.message + "'" ,
+ index: this.index
+ };
+ }
+ return this;
+ }
+};
+
+})(require('carto/tree'));
+
diff --git a/lib/carto/tree/keyword.js b/lib/carto/tree/keyword.js
new file mode 100644
index 0000000..a57429b
--- /dev/null
+++ b/lib/carto/tree/keyword.js
@@ -0,0 +1,17 @@
+(function(tree) {
+
+tree.Keyword = function Keyword(value) {
+ this.value = value;
+ var special = {
+ 'transparent': 'color',
+ 'true': 'boolean',
+ 'false': 'boolean'
+ };
+ this.is = special[value] ? special[value] : 'keyword';
+};
+tree.Keyword.prototype = {
+ eval: function() { return this },
+ toString: function() { return this.value }
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/layer.js b/lib/carto/tree/layer.js
new file mode 100644
index 0000000..c443f9b
--- /dev/null
+++ b/lib/carto/tree/layer.js
@@ -0,0 +1,30 @@
+(function(tree) {
+
+tree.Layer = function Layer(obj) {
+ this.id = obj.id;
+ this.name = obj.name;
+ this.styles = obj.styles;
+ this.srs = obj.srs;
+ this.datasource = obj.Datasource;
+};
+
+tree.Layer.prototype.toXML = function() {
+ var dsoptions = [];
+ for (var i in this.datasource) {
+ dsoptions.push('<Parameter name="' + i + '">' +
+ this.datasource[i] + '</Parameter>');
+ }
+ return '<Layer\n ' +
+ 'id="' + this.id + '"\n' +
+ ' name="' + this.name + '"\n' +
+ ' srs="' + this.srs + '">\n ' +
+ this.styles.reverse().map(function(s) {
+ return '<StyleName>' + s + '</StyleName>';
+ }).join('\n ') +
+ '\n <Datasource>\n ' +
+ dsoptions.join('\n ') +
+ '\n </Datasource>\n' +
+ ' </Layer>\n';
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/mixin.js b/lib/carto/tree/mixin.js
new file mode 100644
index 0000000..0ce5612
--- /dev/null
+++ b/lib/carto/tree/mixin.js
@@ -0,0 +1,99 @@
+(function(tree) {
+
+tree.mixin = {};
+tree.mixin.Call = function Call(elements, args, index) {
+ this.selector = new tree.Selector(null, null, elements);
+ this.arguments = args;
+ this.index = index;
+};
+tree.mixin.Call.prototype = {
+ eval: function(env) {
+ var mixins, rules = [], match = false;
+
+ for (var i = 0; i < env.frames.length; i++) {
+ if ((mixins = env.frames[i].find(this.selector)).length > 0) {
+ for (var m = 0; m < mixins.length; m++) {
+ if (mixins[m].match(this.arguments, env)) {
+ try {
+ Array.prototype.push.apply(
+ rules, mixins[m].eval(env, this.arguments).rules);
+ match = true;
+ } catch (e) {
+ throw { message: e.message, index: e.index, stack: e.stack, call: this.index };
+ }
+ }
+ }
+ if (match) {
+ return rules;
+ } else {
+ throw { message: 'No matching definition was found for `' +
+ this.selector.toString().trim() + '(' +
+ this.arguments.map(function(a) {
+ return a.toString();
+ }).join(', ') + ')`',
+ index: this.index };
+ }
+ }
+ }
+ throw { message: this.selector.toString().trim() + ' is undefined',
+ index: this.index };
+ }
+};
+
+tree.mixin.Definition = function Definition(name, params, rules) {
+ this.name = name;
+ this.selectors = [new tree.Selector(null, null, [new tree.Element(null, name)])];
+ this.params = params;
+ this.arity = params.length;
+ this.rules = rules;
+ this._lookups = {};
+ this.required = params.reduce(function(count, p) {
+ if (p.name && !p.value) { return count + 1 }
+ else { return count }
+ }, 0);
+ this.parent = tree.Ruleset.prototype;
+ this.frames = [];
+};
+tree.mixin.Definition.prototype = {
+ toString: function() { return '' },
+ variable: function(name) { return this.parent.variable.call(this, name) },
+ variables: function() { return this.parent.variables.call(this) },
+ find: function() { return this.parent.find.apply(this, arguments) },
+ rulesets: function() { return this.parent.rulesets.apply(this) },
+
+ eval: function(env, args) {
+ var frame = new tree.Ruleset(null, []), context;
+
+ for (var i = 0, val; i < this.params.length; i++) {
+ if (this.params[i].name) {
+ if (val = (args && args[i]) || this.params[i].value) {
+ frame.rules.unshift(new tree.Rule(this.params[i].name, val.eval(env)));
+ } else {
+ throw { message: 'wrong number of arguments for ' + this.name +
+ ' (' + args.length + ' for ' + this.arity + ')' };
+ }
+ }
+ }
+ return new tree.Ruleset(null, this.rules.slice(0)).eval({
+ frames: [this, frame].concat(this.frames, env.frames)
+ });
+ },
+ match: function(args, env) {
+ var argsLength = (args && args.length) || 0, len;
+
+ if (argsLength < this.required) { return false }
+
+ len = Math.min(argsLength, this.arity);
+
+ for (var i = 0; i < len; i++) {
+ if (!this.params[i].name) {
+ if (args[i].eval(env).toString() != this.params[i].value.eval(env).toString()) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/operation.js b/lib/carto/tree/operation.js
new file mode 100644
index 0000000..879a419
--- /dev/null
+++ b/lib/carto/tree/operation.js
@@ -0,0 +1,37 @@
+(function(tree) {
+
+
+tree.Operation = function Operation(op, operands, index) {
+ this.op = op.trim();
+ this.operands = operands;
+ this.index = index;
+};
+tree.Operation.prototype.eval = function(env) {
+ var a = this.operands[0].eval(env),
+ b = this.operands[1].eval(env),
+ temp;
+
+ if (a instanceof tree.Dimension && b instanceof tree.Color) {
+ if (this.op === '*' || this.op === '+') {
+ temp = b, b = a, a = temp;
+ } else {
+ throw {
+ name: "OperationError",
+ message: "Can't substract or divide a color from a number",
+ index: this.index
+ };
+ }
+ }
+ return a.operate(this.op, b);
+};
+
+tree.operate = function(op, a, b) {
+ switch (op) {
+ case '+': return a + b;
+ case '-': return a - b;
+ case '*': return a * b;
+ case '/': return a / b;
+ }
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/quoted.js b/lib/carto/tree/quoted.js
new file mode 100644
index 0000000..bd1f095
--- /dev/null
+++ b/lib/carto/tree/quoted.js
@@ -0,0 +1,17 @@
+(function(tree) {
+
+tree.Quoted = function Quoted(str, content) {
+ this.value = content || '';
+ this.quote = str.charAt(0);
+ this.is = 'string';
+};
+tree.Quoted.prototype = {
+ toString: function(quotes) {
+ return (quotes === true) ? "'" + this.value + "'" : this.value;
+ },
+ eval: function() {
+ return this;
+ }
+};
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/reference.js b/lib/carto/tree/reference.js
new file mode 100644
index 0000000..9f5dff7
--- /dev/null
+++ b/lib/carto/tree/reference.js
@@ -0,0 +1,156 @@
+(function(tree) {
+var fs = require('fs');
+
+tree.Reference = {
+ data: JSON.parse(fs.readFileSync(__dirname + '/reference.json'))
+}
+
+for (var i in tree.Reference.data.colors) {
+ var c = tree.Reference.data.colors[i];
+ tree.Reference.data.colors[i] = new(tree.Color)(c);
+}
+
+tree.Reference.color_frames = function() {
+ var variables = tree.Reference.data.colors;
+ variables = Object.keys(variables).map(function (k) {
+ var value = variables[k];
+ if (!(value instanceof tree.Value)) {
+ if (!(value instanceof tree.Expression)) {
+ value = new(tree.Expression)([value]);
+ }
+ value = new(tree.Value)([value]);
+ }
+ return new(tree.Rule)('@' + k, value, false, 0);
+ });
+ return variables;
+};
+
+tree.Reference.required_prop_list_cache = {};
+
+tree.Reference.selectors = tree.Reference.selectors || (function() {
+ var list = [];
+ for (var i in tree.Reference.data.symbolizers) {
+ for (var j in tree.Reference.data.symbolizers[i]) {
+ if (tree.Reference.data.symbolizers[i][j].hasOwnProperty('css')) {
+ list.push(tree.Reference.data.symbolizers[i][j].css);
+ }
+ }
+ }
+ return list;
+})();
+
+tree.Reference.validSelector = function(selector) {
+ return tree.Reference.selectors.indexOf(selector) !== -1;
+};
+
+tree.Reference.selectorName = function(selector) {
+ for (var i in tree.Reference.data.symbolizers) {
+ for (var j in tree.Reference.data.symbolizers[i]) {
+ if (selector == tree.Reference.data.symbolizers[i][j].css) {
+ return j;
+ }
+ }
+ }
+};
+
+tree.Reference.selector = function(selector) {
+ for (var i in tree.Reference.data.symbolizers) {
+ for (var j in tree.Reference.data.symbolizers[i]) {
+ if (selector == tree.Reference.data.symbolizers[i][j].css) {
+ return tree.Reference.data.symbolizers[i][j];
+ }
+ }
+ }
+};
+
+tree.Reference.symbolizer = function(selector) {
+ for (var i in tree.Reference.data.symbolizers) {
+ for (var j in tree.Reference.data.symbolizers[i]) {
+ if (selector == tree.Reference.data.symbolizers[i][j].css) {
+ return i;
+ }
+ }
+ }
+};
+
+tree.Reference.requiredPropertyList = function(symbolizer_name) {
+ if (this.required_prop_list_cache[symbolizer_name]) {
+ return this.required_prop_list_cache[symbolizer_name];
+ }
+ var properties = [];
+ for (var j in tree.Reference.data.symbolizers[symbolizer_name]) {
+ if (tree.Reference.data.symbolizers[symbolizer_name][j].required) {
+ properties.push(tree.Reference.data.symbolizers[symbolizer_name][j].css);
+ }
+ }
+ return this.required_prop_list_cache[symbolizer_name] = properties;
+};
+
+tree.Reference.requiredProperties = function(symbolizer_name, rules) {
+ var req = tree.Reference.requiredPropertyList(symbolizer_name);
+ for (i in req) {
+ if (!(req[i] in rules)) {
+ return 'Property ' + req[i] + ' required for defining '
+ + symbolizer_name + ' styles.';
+ }
+ }
+};
+
+/**
+ * TODO: finish implementation - this is dead code
+ */
+tree.Reference._validateValue = {
+ 'font': function(env, value) {
+ if (env.validation_data && env.validation_data.fonts) {
+ return env.validation_data.fonts.indexOf(value) != -1;
+ } else {
+ return true;
+ }
+ }
+};
+
+tree.Reference.isFont = function(selector) {
+ return tree.Reference.selector(selector).validate == 'font';
+}
+
+tree.Reference.validValue = function(env, selector, value) {
+ if (value[0]) {
+ return tree.Reference.selector(selector).type == value[0].is;
+ } else {
+ // TODO: handle in reusable way
+ if (value.value[0].is == 'keyword') {
+ return tree.Reference
+ .selector(selector).type
+ .indexOf(value.value[0].value) !== -1;
+ } else if (value.value[0].is == 'undefined') {
+ // caught earlier in the chain - ignore here so that
+ // error is not overridden
+ return true;
+ } else if (tree.Reference.selector(selector).type == 'numbers') {
+ for (i in value.value) {
+ if (value.value[i].is !== 'float') {
+ return false;
+ }
+ }
+ return true;
+ } else {
+ if (tree.Reference.selector(selector).validate) {
+ var valid = false;
+ for (var i = 0; i < value.value.length; i++) {
+ if (tree.Reference.selector(selector).type == value.value[i].is &&
+ tree.Reference
+ ._validateValue
+ [tree.Reference.selector(selector).validate]
+ (env, value.value[i].value)) {
+ return true;
+ }
+ }
+ return valid;
+ } else {
+ return tree.Reference.selector(selector).type == value.value[0].is;
+ }
+ }
+ }
+}
+
+})(require('carto/tree'));
diff --git a/lib/carto/tree/reference.json b/lib/carto/tree/reference.json
new file mode 100644
index 0000000..3a9e857
--- /dev/null
+++ b/lib/carto/tree/reference.json
@@ -0,0 +1,869 @@
+{
+ "style": {
+ "name": {
+ },
+ "filter-mode": {
+ }
+ },
+ "meta-writer": {
+ "name": {
+ },
+ "type": {
+ },
+ "file": {
+ },
+ "default-output": {
+ },
+ "output-empty": {
+ }
+ },
+ "font-set": {
+ "name": {
+ },
+ "font": {
+ "face-name": {
+ }
+ }
+ },
+ "layer" : {
+ "name": {
+ },
+ "srs": {
+ },
+ "status": {
+ },
+ "title": {
+ },
+ "abstract": {
+ },
+ "minzoom": {
+ },
+ "maxzoom": {
+ },
+ "queryable": {
+ },
+ "clear-label-cache": {
+ }
+ },
+ "symbolizers" : {
+ "map": {
+ "background-color": {
+ "css": "background-color",
+ "default-value": "none",
+ "default-meaning": "transparent",
+ "type": "color",
+ "doc": "Map Background color"
+ },
+ "background-image": {
+ "css": "background-image",
+ "type": "uri",
+ "description": "Map Background image"
+ },
+ "srs": {
+ "css": "srs",
+ "type": "string",
+ "description": "Map spatial reference (proj4 string)"
+ },
+ "buffer-size": {
+ "css": "buffer",
+ "api": "buffer-size",
+ "type": "uri",
+ "default-value": "0",
+ "default-meaning": "No buffer will be used",
+ "description": "Extra tolerance around the map (in pixels) used to ensure labels crossing tile boundaries are equally rendered in each tile (e.g. cut in each tile). Not intended to be used in combination with 'avoid-edges'."
+ },
+ "paths-from-xml": {
+ "css": "",
+ "api": "",
+ "default-value": "true",
+ "default-meaning": "Paths read from XML will be interpreted from the location of the XML",
+ "type": "boolean",
+ "doc": "value to control whether paths in the XML will be interpreted from the location of the XML or from the working directory of the program that calls load_map()"
+ },
+ "minimum-version": {
+ "css": "",
+ "api": "",
+ "default-value": "none",
+ "default-meaning": "Mapnik version will not be detected and no error will be thrown about compatibility",
+ "type": "string",
+ "doc": "The minumum Mapnik version (e.g. 0.7.2) needed to use certain functionality in the stylesheet"
+ },
+ "font-directory": {
+ "css": "font-directory",
+ "type": "uri",
+ "api": "",
+ "default-value": "none",
+ "default-meaning": "No fonts will be registered",
+ "doc": "Path to a directory which holds fonts which should be registered when the Map is loaded"
+ }
+ },
+ "polygon": {
+ "fill": {
+ "css": "polygon-fill",
+ "api": "fill",
+ "type": "color",
+ "availability": "0.5.1",
+ "default-value": "rgb(128,128,128)",
+ "default-meaning": "grey",
+ "doc": "Fill color to assign to a polygon"
+ },
+ "gamma": {
+ "css": "polygon-gamma",
+ "api": "gamma",
+ "type": "float",
+ "availability": "0.7.0",
+ "default-value": 1,
+ "default-meaning": "fully antialiased",
+ "range": "0-1",
+ "doc": "Level of antialiasing of polygon edges"
+ },
+ "fill-opacity": {
+ "css": "polygon-opacity",
+ "type": "float",
+ "default-value": 1,
+ "default-meaning": "opaque"
+ },
+ "meta-output": {
+ "css": "polygon-meta-output",
+ "type": "string",
+ "default-value": "",
+ "default-meaning": "No MetaWriter Output"
+ },
+ "meta-writer": {
+ "css": "polygon-meta-writer",
+ "type": "string",
+ "default-value": "",
+ "default-meaning": "No MetaWriter specified"
+ }
+ },
+ "line": {
+ "stroke": {
+ "css": "line-color",
+ "default-value": "black",
+ "type": "color",
+ "doc": "The color of a drawn line"
+ },
+ "stroke-width": {
+ "css": "line-width",
+ "default-value": 1,
+ "type": "float",
+ "doc": "The width of a line in pixels"
+ },
+ "stroke-opacity": {
+ "css": "line-opacity",
+ "default-value": 1,
+ "type": "float",
+ "default-meaning": "opaque",
+ "doc": "The opacity of a line"
+ },
+ "stroke-linejoin": {
+ "css": "line-join",
+ "default-value": "miter",
+ "type": [
+ "miter",
+ "round",
+ "bevel"
+ ],
+ "doc": "The behavior of lines when joining"
+ },
+ "stroke-linecap": {
+ "css": "line-cap",
+ "default-value": "butt",
+ "type": [
+ "butt",
+ "round",
+ "square"
+ ],
+ "doc": "The display of line endings"
+ },
+ "stroke-gamma": {
+ "css": "line-gamma",
+ "api": "gamma",
+ "type": "float",
+ "availability": "2.0.0",
+ "default-value": 1,
+ "default-meaning": "fully antialiased",
+ "range": "0-1",
+ "doc": "Level of antialiasing of stroke line"
+ },
+ "stroke-dasharray": {
+ "css": "line-dasharray",
+ "type": "numbers",
+ "doc": "A pair of length values [a,b], where (a) is the dash length and (b) is the gap length respectively. More than two values are supported for more complex patterns.",
+ "default-value": "none",
+ "default-meaning": "solid line"
+ },
+ "meta-output": {
+ "css": "line-meta-output",
+ "type": "string",
+ "default-value": "",
+ "default-meaning": "No MetaWriter Output"
+ },
+ "meta-writer": {
+ "css": "line-meta-writer",
+ "type": "string",
+ "default-value": ""
+ }
+ },
+ "markers": {
+ "file": {
+ "css": "marker-file",
+ "type": "uri"
+ },
+ "opacity": {
+ "css": "marker-opacity",
+ "default-value": 1,
+ "default-meaning": "opaque",
+ "type": "float"
+ },
+ "stroke": {
+ "css": "marker-line-color",
+ "type": "color"
+ },
+ "stroke-width": {
+ "css": "marker-line-width",
+ "type": "float"
+ },
+ "stroke-opacity": {
+ "css": "marker-line-opacity",
+ "default-value": 1,
+ "default-meaning": "opaque",
+ "type": "float"
+ },
+ "placement": {
+ "css": "marker-placement",
+ "type": [
+ "point",
+ "line"
+ ]
+ },
+ "type": {
+ "css": "marker-type",
+ "type": [
+ "arrow",
+ "ellipse"
+ ]
+ },
+ "width": {
+ "css": "marker-width",
+ "type": "float"
+ },
+ "height": {
+ "css": "marker-height",
+ "type": "float"
+ },
+ "fill": {
+ "css": "marker-fill",
+ "type": "color"
+ },
+ "allow-overlap": {
+ "css": "marker-allow-overlap",
+ "type": "boolean",
+ "default-value": "false",
+ "default-meaning": "do not allow overlap"
+ },
+ "spacing": {
+ "css": "marker-spacing",
+ "docs": "Space between repeated labels",
+ "type": "float"
+ },
+ "max-error": {
+ "css": "marker-max-error",
+ "type": "float"
+ },
+ "transform": {
+ "css": "marker-transform",
+ "type": "string"
+ },
+ "meta-output": {
+ "css": "marker-meta-output",
+ "type": "string",
+ "default-value": "",
+ "default-meaning": "No MetaWriter Output"