diff --git a/lib/carto/index.js b/lib/carto/index.js
index a0265d848..e79a614c8 100644
--- a/lib/carto/index.js
+++ b/lib/carto/index.js
@@ -66,9 +66,10 @@ var carto = {
[ 'call', 'color', 'comment', 'definition', 'dimension',
'element', 'expression', 'filterset', 'filter', 'field',
- 'keyword', 'layer', 'literal', 'operation', 'quoted', 'imagefilter',
- 'reference', 'rule', 'ruleset', 'selector', 'style', 'url', 'value',
- 'variable', 'zoom', 'invalid', 'fontset'
+ 'keyword', 'layer', 'literal', 'operation', 'pseudo',
+ 'quoted', 'imagefilter', 'reference', 'rendernode',
+ 'rule', 'ruleset', 'selector', 'style', 'subset', 'url',
+ 'value', 'variable', 'zoom', 'invalid', 'fontset'
].forEach(function(n) {
require('./tree/' + n);
});
diff --git a/lib/carto/parser.js b/lib/carto/parser.js
index 2327576eb..a4acfa732 100644
--- a/lib/carto/parser.js
+++ b/lib/carto/parser.js
@@ -295,7 +295,7 @@ carto.Parser = function Parser(env) {
primary: function() {
var node, root = [];
- while ((node = $(this.rule) || $(this.ruleset) ||
+ while ((node = $(this.rule) || $(this.ruleset) || $(this.subset) ||
$(this.comment)) ||
$(/^[\s\n]+/) || (node = $(this.invalid))) {
if (node) root.push(node);
@@ -602,6 +602,34 @@ carto.Parser = function Parser(env) {
}
},
+ pseudo: function() {
+ var s = $(/^::([\w\-]+)(?:\(([0-9]+)\))?/);
+ if (s) return new tree.Pseudo(s[1], s[2]);
+ },
+
+ subset: function() {
+ var symbolizer,
+ p, pseudoelements = [];
+
+ save();
+
+ symbolizer = $(/^[\w\-]+(?:\/[\w\-]+)*/);
+ while (p = $(this.pseudo)) {
+ pseudoelements.push(p);
+ }
+ if (pseudoelements.length > 0 && $('{')) {
+ var node, rules = [], subsets = [];
+
+ while ((node = $(this.rule) || $(this.subset) || $(this.comment)) || $(/^[\s\n]+/)) {
+ rules.push(node);
+ }
+ if ($('}')) {
+ return new tree.Subset(symbolizer, pseudoelements, rules);
+ }
+ }
+ restore();
+ },
+
// The `block` rule is used by `ruleset`
// It's a wrapper around the `primary` rule, with added `{}`.
block: function() {
@@ -614,7 +642,7 @@ carto.Parser = function Parser(env) {
// div, .class, body > p {...}
ruleset: function() {
- var selectors = [], s, f, l, rules, filters = [];
+ var selectors = [], s, sub, rules, filters = [];
save();
while (s = $(this.selector)) {
@@ -627,7 +655,7 @@ carto.Parser = function Parser(env) {
while ($(this.comment)) {}
}
- if (selectors.length > 0 && (rules = $(this.block))) {
+ if ((selectors.length > 0 || sub) && (rules = $(this.block))) {
if (selectors.length === 1 &&
selectors[0].elements.length &&
selectors[0].elements[0].value === 'Map') {
diff --git a/lib/carto/renderer.js b/lib/carto/renderer.js
index dc5df7d2e..d7a39d67d 100644
--- a/lib/carto/renderer.js
+++ b/lib/carto/renderer.js
@@ -227,6 +227,7 @@ carto.Renderer.prototype.render = function render(m) {
function addRules(current, definition, byFilter, env) {
var newFilters = definition.filters,
newRules = definition.rules,
+ newSubdef = definition.subdefinitions,
updatedFilters, clone, previous;
// The current definition might have been split up into
diff --git a/lib/carto/tree/definition.js b/lib/carto/tree/definition.js
index 140888779..cb8b74711 100644
--- a/lib/carto/tree/definition.js
+++ b/lib/carto/tree/definition.js
@@ -37,6 +37,7 @@ tree.Definition.prototype.clone = function(filters) {
var clone = Object.create(tree.Definition.prototype);
clone.rules = this.rules.slice();
clone.ruleIndex = _.clone(this.ruleIndex);
+ clone.subdefinitions = _.clone(this.subdefinitions);
clone.filters = filters ? filters : this.filters.clone();
clone.attachment = this.attachment;
return clone;
@@ -70,12 +71,6 @@ tree.Definition.prototype.appliesTo = function(id, classes) {
return true;
};
-function symbolizerName(symbolizer) {
- function capitalize(str) { return str[1].toUpperCase(); }
- return symbolizer.charAt(0).toUpperCase() +
- symbolizer.slice(1).replace(/\-./, capitalize) + 'Symbolizer';
-}
-
// Get a simple list of the symbolizers, in order
function symbolizerList(sym_order) {
return sym_order.sort(function(a, b) { return a[1] - b[1]; })
@@ -89,8 +84,8 @@ tree.Definition.prototype.symbolizersToXML = function(env, symbolizers, zoom) {
var sym_order = [], indexes = [];
for (var key in symbolizers) {
indexes = [];
- for (var prop in symbolizers[key]) {
- indexes.push(symbolizers[key][prop].index);
+ for (var prop in symbolizers[key].attributes) {
+ indexes.push(symbolizers[key].attributes[prop].index);
}
var min_idx = Math.min.apply(Math, indexes);
sym_order.push([key, min_idx]);
@@ -100,55 +95,16 @@ tree.Definition.prototype.symbolizersToXML = function(env, symbolizers, zoom) {
var sym_count = 0;
for (var i = 0; i < sym_order.length; i++) {
- var attributes = symbolizers[sym_order[i]];
- var symbolizer = sym_order[i].split('/').pop();
+ var symbolizer = symbolizers[sym_order[i]];
// Skip the magical * symbolizer which is used for universal properties
// which are bubbled up to Style elements intead of Symbolizer elements.
- if (symbolizer === '*') continue;
+ if (symbolizer.name === '*') continue;
sym_count++;
- var fail = tree.Reference.requiredProperties(symbolizer, attributes);
- if (fail) {
- var rule = attributes[Object.keys(attributes).shift()];
- env.error({
- message: fail,
- index: rule.index,
- filename: rule.filename
- });
- }
-
- var name = symbolizerName(symbolizer);
-
- var selfclosing = true, tagcontent;
- xml += ' <' + name + ' ';
- for (var j in attributes) {
- if (symbolizer === 'map') env.error({
- message: 'Map properties are not permitted in other rules',
- index: attributes[j].index,
- filename: attributes[j].filename
- });
- var x = tree.Reference.selector(attributes[j].name);
- if (x && x.serialization && x.serialization === 'content') {
- selfclosing = false;
- tagcontent = attributes[j].ev(env).toXML(env, true);
- } else if (x && x.serialization && x.serialization === 'tag') {
- selfclosing = false;
- tagcontent = attributes[j].ev(env).toXML(env, true);
- } else {
- xml += attributes[j].ev(env).toXML(env) + ' ';
- }
- }
- if (selfclosing) {
- xml += '/>\n';
- } else if (typeof tagcontent !== "undefined") {
- if (tagcontent.indexOf('<') != -1) {
- xml += '>' + tagcontent + '' + name + '>\n';
- } else {
- xml += '>' + name + '>\n';
- }
- }
+ xml += symbolizer.toXML(env);
}
+
if (!sym_count || !xml) return '';
return ' \n' + xml + ' \n';
};
@@ -161,14 +117,13 @@ tree.Definition.prototype.collectSymbolizers = function(zooms, i) {
for (var j = i; j < this.rules.length; j++) {
child = this.rules[j];
var key = child.instance + '/' + child.symbolizer;
- if (zooms.current & child.zoom &&
- (!(key in symbolizers) ||
- (!(child.name in symbolizers[key])))) {
- zooms.current &= child.zoom;
+ if (zooms.current & child.zoom) {
if (!(key in symbolizers)) {
- symbolizers[key] = {};
+ symbolizers[key] = new tree.SymbolizerNode(child.symbolizer);
+ }
+ if (symbolizers[key].addAttribute(child)) {
+ zooms.current &= child.zoom;
}
- symbolizers[key][child.name] = child;
}
}
diff --git a/lib/carto/tree/pseudo.js b/lib/carto/tree/pseudo.js
new file mode 100644
index 000000000..b55d1dace
--- /dev/null
+++ b/lib/carto/tree/pseudo.js
@@ -0,0 +1,8 @@
+(function(tree) {
+
+tree.Pseudo = function Pseudo(name, index) {
+ this.name = name;
+ this.index = index || 0;
+};
+
+})(require('../tree'));
diff --git a/lib/carto/tree/reference.js b/lib/carto/tree/reference.js
index e6ef87264..e7b876c81 100644
--- a/lib/carto/tree/reference.js
+++ b/lib/carto/tree/reference.js
@@ -11,6 +11,8 @@ var _ = require('underscore'),
ref.setData = function(data) {
ref.data = data;
ref.selector_cache = generateSelectorCache(data);
+ ref.child_cache = generateChildTypes(data);
+ ref.pseudo_cache = generatePseudoElementCache(data);
ref.mapnikFunctions = generateMapnikFunctions(data);
ref.required_cache = generateRequiredProperties(data);
};
@@ -24,21 +26,66 @@ ref.setVersion = function(version) {
}
};
-ref.selectorData = function(selector, i) {
- if (ref.selector_cache[selector]) return ref.selector_cache[selector][i];
+ref.selectorData = function(selector, element, i) {
+ if (element) {
+ if (ref.selector_cache.elements[element] &&
+ ref.selector_cache.elements[element][selector]) {
+ return ref.selector_cache.elements[element][selector][i];
+ }
+ }
+ else if (ref.selector_cache.symbolizers[selector]) {
+ return ref.selector_cache.symbolizers[selector][i];
+ }
+};
+
+ref.pseudoElementData = function(pseudo, i) {
+ if (ref.pseudo_cache[pseudo]) return ref.pseudo_cache[pseudo][i];
};
-ref.validSelector = function(selector) { return !!ref.selector_cache[selector]; };
-ref.selectorName = function(selector) { return ref.selectorData(selector, 2); };
-ref.selector = function(selector) { return ref.selectorData(selector, 0); };
-ref.symbolizer = function(selector) { return ref.selectorData(selector, 1); };
+ref.validSelector = function(selector, element) {
+ if (element) {
+ return !!ref.selector_cache.elements[element][selector];
+ }
+ return !!ref.selector_cache.symbolizers[selector];
+};
+ref.selectorName = function(selector, element) { return ref.selectorData(selector, element, 2); };
+ref.selector = function(selector, element) { return ref.selectorData(selector, element, 0); };
+ref.selectorType = function(selector, element) { return ref.selectorData(selector, element, 1); };
+
+ref.validElement = function(pseudo) { return !!ref.pseudo_cache[pseudo]; };
+ref.elementName = function(pseudo) { return ref.pseudoElementData(pseudo, 1); };
+ref.pseudoProperties = function(pseudo) { return ref.pseudoElementData(pseudo, 0); };
+
+ref.elementSelectorName = function(type, selector) {
+ return type + '/' + selector;
+};
function generateSelectorCache(data) {
- var index = {};
+ var index = {'symbolizers': {}, 'elements': {}};
for (var i in data.symbolizers) {
for (var j in data.symbolizers[i]) {
if (data.symbolizers[i][j].hasOwnProperty('css')) {
- index[data.symbolizers[i][j].css] = [data.symbolizers[i][j], i, j];
+ index.symbolizers[data.symbolizers[i][j].css] = [data.symbolizers[i][j], i, j];
+ }
+ }
+ }
+ for (var i in data.elements) {
+ index.elements[i] = {};
+ for (var j in data.elements[i]) {
+ if (data.elements[i][j].hasOwnProperty('css')) {
+ index.elements[i][data.elements[i][j].css] = [data.elements[i][j], i, j];
+ }
+ }
+ }
+ return index;
+}
+
+function generatePseudoElementCache(data) {
+ var index = {};
+ for (var i in data.elements) {
+ if (data.elements[i].pseudo) {
+ for (var j in data.elements[i].pseudo) {
+ index[j] = [data.elements[i].pseudo[j], i];
}
}
}
@@ -57,9 +104,40 @@ function generateMapnikFunctions(data) {
}
}
}
+ for (var i in data.elements) {
+ for (var j in data.elements[i]) {
+ if (data.elements[i][j].type === 'functions') {
+ for (var k = 0; k < data.elements[i][j].functions.length; k++) {
+ var fn = data.elements[i][j].functions[k];
+ functions[fn[0]] = fn[1];
+ }
+ }
+ }
+ }
return functions;
}
+function generateChildTypes(data) {
+ var child_elements, cache = {};
+ for (var name in data.symbolizers) {
+ child_elements = data.symbolizers[name]['child-elements'];
+ if (child_elements && child_elements.length) {
+ cache[name] = child_elements;
+ }
+ }
+ for (var name in data.elements) {
+ child_elements = data.elements[name]['child-elements'];
+ if (child_elements && child_elements.length) {
+ cache[name] = child_elements;
+ }
+ }
+ return cache;
+}
+
+ref.childTypes = function(name) {
+ return ref.child_cache[name];
+}
+
function generateRequiredProperties(data) {
var cache = {};
for (var symbolizer_name in data.symbolizers) {
@@ -70,6 +148,14 @@ function generateRequiredProperties(data) {
}
}
}
+ for (var element_name in data.elements) {
+ cache[element_name] = [];
+ for (var j in data.elements[element_name]) {
+ if (data.elements[element_name][j].required) {
+ cache[element_name].push(data.elements[element_name][j].css);
+ }
+ }
+ }
return cache;
}
@@ -94,8 +180,8 @@ ref._validateValue = {
}
};
-ref.isFont = function(selector) {
- return ref.selector(selector).validate == 'font';
+ref.isFont = function(selector, element) {
+ return ref.selector(selector, element).validate == 'font';
};
// https://gist.github.com/982927
@@ -119,13 +205,13 @@ ref.editDistance = function(a, b){
return matrix[b.length][a.length];
};
-function validateFunctions(value, selector) {
+function validateFunctions(value, selector, element) {
if (value.value[0].is === 'string') return true;
for (var i in value.value) {
for (var j in value.value[i].value) {
if (value.value[i].value[j].is !== 'call') return false;
var f = _.find(ref
- .selector(selector).functions, function(x) {
+ .selector(selector, element).functions, function(x) {
return x[0] == value.value[i].value[j].name;
});
if (!(f && f[1] == -1)) {
@@ -137,35 +223,35 @@ function validateFunctions(value, selector) {
return true;
}
-function validateKeyword(value, selector) {
- if (typeof ref.selector(selector).type === 'object') {
- return ref.selector(selector).type
+function validateKeyword(value, selector, element) {
+ if (typeof ref.selector(selector, element).type === 'object') {
+ return ref.selector(selector, element).type
.indexOf(value.value[0].value) !== -1;
} else {
// allow unquoted keywords as strings
- return ref.selector(selector).type === 'string';
+ return ref.selector(selector, element).type === 'string';
}
}
-ref.validValue = function(env, selector, value) {
+ref.validValue = function(env, selector, element, value) {
var i, j;
// TODO: handle in reusable way
- if (!ref.selector(selector)) {
+ if (!ref.selector(selector, element)) {
return false;
} else if (value.value[0].is == 'keyword') {
- return validateKeyword(value, selector);
+ return validateKeyword(value, selector, element);
} else if (value.value[0].is == 'undefined') {
// caught earlier in the chain - ignore here so that
// error is not overridden
return true;
- } else if (ref.selector(selector).type == 'numbers') {
+ } else if (ref.selector(selector, element).type == 'numbers') {
for (i in value.value) {
if (value.value[i].is !== 'float') {
return false;
}
}
return true;
- } else if (ref.selector(selector).type == 'tags') {
+ } else if (ref.selector(selector, element).type == 'tags') {
if (!value.value) return false;
if (!value.value[0].value) {
return value.value[0].is === 'tag';
@@ -174,13 +260,13 @@ ref.validValue = function(env, selector, value) {
if (value.value[0].value[i].is !== 'tag') return false;
}
return true;
- } else if (ref.selector(selector).type == 'functions') {
+ } else if (ref.selector(selector, element).type == 'functions') {
// For backwards compatibility, you can specify a string for `functions`-compatible
// values, though they will not be validated.
- return validateFunctions(value, selector);
- } else if (ref.selector(selector).type === 'expression') {
+ return validateFunctions(value, selector, element);
+ } else if (ref.selector(selector, element).type === 'expression') {
return true;
- } else if (ref.selector(selector).type === 'unsigned') {
+ } else if (ref.selector(selector, element).type === 'unsigned') {
if (value.value[0].is === 'float') {
value.value[0].round();
return true;
@@ -188,20 +274,20 @@ ref.validValue = function(env, selector, value) {
return false;
}
} else {
- if (ref.selector(selector).validate) {
+ if (ref.selector(selector, element).validate) {
var valid = false;
for (i = 0; i < value.value.length; i++) {
- if (ref.selector(selector).type == value.value[i].is &&
+ if (ref.selector(selector, element).type == value.value[i].is &&
ref
._validateValue
- [ref.selector(selector).validate]
+ [ref.selector(selector, element).validate]
(env, value.value[i].value)) {
return true;
}
}
return valid;
} else {
- return ref.selector(selector).type == value.value[0].is;
+ return ref.selector(selector, element).type == value.value[0].is;
}
}
};
diff --git a/lib/carto/tree/rendernode.js b/lib/carto/tree/rendernode.js
new file mode 100644
index 000000000..d22679d20
--- /dev/null
+++ b/lib/carto/tree/rendernode.js
@@ -0,0 +1,184 @@
+var sys = require('sys');
+
+(function(tree) {
+
+tree.RenderNode = function RenderNode(name, pseudoname, index) {
+ this.name = name || '';
+ this.pseudoname = pseudoname || '';
+ this.index = index || 0;
+ this.attributes = {};
+ this.children = {};
+};
+
+tree.RenderNode.prototype.addAttribute = function(rule) {
+ if (rule.pseudoelements.length) {
+ return this.addSubAttribute(rule.pseudoelements.slice(), rule);
+ }
+ if (rule.name in this.attributes) {
+ return false;
+ }
+ this.attributes[rule.name] = rule;
+ return true;
+};
+
+tree.RenderNode.prototype.addSubAttribute = function(pseudoelements, rule) {
+ if (pseudoelements.length) {
+ var pseudo = pseudoelements.shift(),
+ type = tree.Reference.elementName(pseudo.name),
+ key = type + '/' + pseudo.name + '/' + pseudo.index;
+ if (!this.children[key]) {
+ this.children[key] = new tree.RenderNode(type, pseudo.name, pseudo.index);
+ }
+ return this.children[key].addSubAttribute(pseudoelements, rule);
+ }
+ if (rule.name in this.attributes) {
+ return false;
+ }
+ this.attributes[rule.name] = rule;
+ return true;
+};
+
+tree.RenderNode.prototype.sortChildren = function(env) {
+ var child, child_types, type_index, pseudo_index,
+ child_order = [];
+ for (var key in this.children) {
+ child = this.children[key];
+ child_types = tree.Reference.childTypes(this.name),
+ type_index = child_types ? child_types.indexOf(child.name) : -1;
+ pseudo_index = parseInt(child.index);
+
+ if (type_index === -1) {
+ env.error({
+ message: "Cannot add pseudo element type '" + child.pseudoname + "' to '" + this.pseudoname + "'.",
+ type: 'syntax'
+ });
+ return [];
+ }
+ if (pseudo_index === NaN || pseudo_index < 0) {
+ env.error({
+ message: "Invalid pseudo element index:'" + child.index + ". Index must be a positive integer.",
+ type: 'syntax'
+ });
+ return [];
+ }
+ child_order.push([key, type_index, pseudo_index]);
+ }
+ return child_order.sort(function (a, b) {
+ if (a[1] === b[1]) {
+ return a[2] - b[2];
+ }
+ return a[1] - b[1];
+ }).map(function(v) { return v[0]; });
+}
+
+tree.RenderNode.prototype.tagName = function(property) {
+ function capitalize(str) { return str[1].toUpperCase(); }
+ return property.charAt(0).toUpperCase() +
+ property.slice(1).replace(/\-./g, capitalize);
+};
+
+function indent(indentation) {
+ return Array(indentation + 1).join(' ');
+}
+
+tree.RenderNode.prototype.toXML = function(env, indentation) {
+ indentation = indentation || 2;
+ var xml = '', fail = tree.Reference.requiredProperties(this.name, this.attributes);
+ if (fail) {
+ var rule = this.attributes[Object.keys(this.attributes).shift()];
+ env.error({
+ message: fail,
+ index: rule.index,
+ filename: rule.filename
+ });
+ }
+
+ var name = this.tagName(this.name);
+
+ var hastags = Object.keys(this.children).length, selfclosing = !hastags,
+ tagcontent = '', beforecontent = '', aftercontent = '', x;
+ xml += indent(indentation) + '<' + name + ' ';
+ for (var j in this.attributes) {
+ if (this.name === 'map') env.error({
+ message: 'Map properties are not permitted in other rules',
+ index: this.attributes[j].index,
+ filename: this.attributes[j].filename
+ });
+ if (this.is === 'SymbolizerNode') {
+ x = tree.Reference.selector(this.attributes[j].name);
+ }
+ else {
+ x = tree.Reference.selector(this.attributes[j].name, this.name);
+ }
+ if (x && x.serialization && (x.serialization === 'content'|| x.serialization === 'tag')) {
+ selfclosing = false;
+ tagcontent = this.attributes[j].ev(env).toXML(env, true);
+ } else {
+ xml += this.attributes[j].ev(env).toXML(env) + ' ';
+ }
+ }
+
+ // Wrap content in CDATA if it does not contain XML tags
+ if (tagcontent !== undefined) {
+ if (tagcontent.indexOf('<') == -1) {
+ tagcontent = '';
+ }
+ else {
+ hastags = true;
+ }
+ }
+
+ // Add child nodes to the XML
+ var child, properties, child_order = this.sortChildren(env);
+ for (var i = 0; i < child_order.length; i++) {
+ child = this.children[child_order[i]];
+ properties = tree.Reference.pseudoProperties(child.pseudoname);
+ console.log('%j', properties);
+ if (properties && properties.behavior === "prepend") {
+ beforecontent = child.toXML(env, indentation + 1) + beforecontent;
+ }
+ else {
+ aftercontent += child.toXML(env, indentation + 1);
+ }
+ }
+
+ if (selfclosing) {
+ xml += '/>\n';
+ }
+ else {
+ xml += '>';
+ if (hastags) {
+ xml += '\n'
+ + beforecontent
+ + indent(indentation + 1)
+ + tagcontent
+ + '\n'
+ + aftercontent
+ + indent(indentation);
+ }
+ else {
+ xml += tagcontent;
+ }
+ xml += '' + name + '>\n';
+ }
+ return xml;
+};
+
+tree.RenderNode.prototype.is = "RenderNode";
+
+// Extend RenderNode for symbolizers,
+// having no index and modified tagName.
+tree.SymbolizerNode = function(name) {
+ this.name = name || '';
+ this.pseudoname = name;
+ this.index = 0;
+ this.attributes = {};
+ this.children = {};
+}
+tree.SymbolizerNode.prototype = new tree.RenderNode();
+tree.SymbolizerNode.prototype.tagName = function(property) {
+ return tree.RenderNode.prototype.tagName(property) + "Symbolizer";
+};
+tree.SymbolizerNode.prototype.is = "SymbolizerNode";
+
+})(require('../tree'));
diff --git a/lib/carto/tree/rule.js b/lib/carto/tree/rule.js
index 75b64b367..a8ffc314b 100644
--- a/lib/carto/tree/rule.js
+++ b/lib/carto/tree/rule.js
@@ -2,16 +2,18 @@
// a rule is a single property and value combination, or variable
// name and value combination, like
// polygon-opacity: 1.0; or @opacity: 1.0;
-tree.Rule = function Rule(name, value, index, filename) {
+tree.Rule = function Rule(name, value, index, filename, pseudoname) {
var parts = name.split('/');
this.name = parts.pop();
this.instance = parts.length ? parts[0] : '__default__';
this.value = (value instanceof tree.Value) ?
value : new tree.Value([value]);
this.index = index;
- this.symbolizer = tree.Reference.symbolizer(this.name);
+ this.symbolizer = tree.Reference.selectorType(this.name);
this.filename = filename;
this.variable = (name.charAt(0) === '@');
+ this.pseudoelements = [];
+ this.pseudoname = pseudoname;
};
tree.Rule.prototype.is = 'rule';
@@ -25,19 +27,26 @@ tree.Rule.prototype.clone = function() {
clone.symbolizer = this.symbolizer;
clone.filename = this.filename;
clone.variable = this.variable;
+ clone.pseudoname = this.pseudoname;
+ clone.pseudoelements = this.pseudoelements.slice();
return clone;
};
tree.Rule.prototype.updateID = function() {
- return this.id = this.zoom + '#' + this.instance + '#' + this.name;
+ var path = this.pseudoelements.map(function (p) {
+ return '#' + p.name + '#' + p.index;
+ }).join();
+ return this.id = this.zoom + '#' + this.instance + path + '#' + this.name;
};
tree.Rule.prototype.toString = function() {
return '[' + tree.Zoom.toString(this.zoom) + '] ' + this.name + ': ' + this.value;
};
-function getMean(name) {
- return Object.keys(tree.Reference.selector_cache).map(function(f) {
+function getMean(name, element) {
+ var cache = element ? tree.Reference.selector_cache.elements[element]
+ : tree.Reference.selector_cache.symbolizers;
+ return Object.keys(cache).map(function(f) {
return [f, tree.Reference.editDistance(name, f)];
}).sort(function(a, b) { return a[1] - b[1]; });
}
@@ -47,8 +56,12 @@ function getMean(name) {
// now this is just for the TextSymbolizer, but applies to other
// properties in reference.json which specify serialization=content
tree.Rule.prototype.toXML = function(env, content, sep, format) {
- if (!tree.Reference.validSelector(this.name)) {
- var mean = getMean(this.name);
+ var elementname;
+ if (this.pseudoname) {
+ elementname = tree.Reference.elementName(this.pseudoname);
+ }
+ if (!tree.Reference.validSelector(this.name, elementname)) {
+ var mean = getMean(this.name, elementname);
var mean_message = '';
if (mean[0][1] < 3) {
mean_message = '. Did you mean ' + mean[0][0] + '?';
@@ -62,8 +75,8 @@ tree.Rule.prototype.toXML = function(env, content, sep, format) {
}
if ((this.value instanceof tree.Value) &&
- !tree.Reference.validValue(env, this.name, this.value)) {
- if (!tree.Reference.selector(this.name)) {
+ !tree.Reference.validValue(env, this.name, elementname, this.value)) {
+ if (!tree.Reference.selector(this.name, elementname)) {
return env.error({
message: 'Unrecognized property: ' +
this.name,
@@ -73,12 +86,12 @@ tree.Rule.prototype.toXML = function(env, content, sep, format) {
});
} else {
var typename;
- if (tree.Reference.selector(this.name).validate) {
- typename = tree.Reference.selector(this.name).validate;
- } else if (typeof tree.Reference.selector(this.name).type === 'object') {
- typename = 'keyword (options: ' + tree.Reference.selector(this.name).type.join(', ') + ')';
+ if (tree.Reference.selector(this.name, elementname).validate) {
+ typename = tree.Reference.selector(this.name, elementname).validate;
+ } else if (typeof tree.Reference.selector(this.name, elementname).type === 'object') {
+ typename = 'keyword (options: ' + tree.Reference.selector(this.name, elementname).type.join(', ') + ')';
} else {
- typename = tree.Reference.selector(this.name).type;
+ typename = tree.Reference.selector(this.name, elementname).type;
}
return env.error({
message: 'Invalid value for ' +
@@ -96,13 +109,13 @@ tree.Rule.prototype.toXML = function(env, content, sep, format) {
if (this.variable) {
return '';
- } else if (tree.Reference.isFont(this.name) && this.value.value.length > 1) {
+ } else if (tree.Reference.isFont(this.name, elementname) && this.value.value.length > 1) {
var f = tree._getFontSet(env, this.value.value);
return 'fontset-name="' + f.name + '"';
} else if (content) {
return this.value.toString(env, this.name, sep);
} else {
- return tree.Reference.selectorName(this.name) +
+ return tree.Reference.selectorName(this.name, elementname) +
'="' +
this.value.toString(env, this.name) +
'"';
@@ -114,7 +127,8 @@ tree.Rule.prototype.ev = function(context) {
return new tree.Rule(this.name,
this.value.ev(context),
this.index,
- this.filename);
+ this.filename,
+ this.pseudoname);
};
})(require('../tree'));
diff --git a/lib/carto/tree/ruleset.js b/lib/carto/tree/ruleset.js
index cf4896524..0977f050e 100644
--- a/lib/carto/tree/ruleset.js
+++ b/lib/carto/tree/ruleset.js
@@ -1,4 +1,5 @@
(function(tree) {
+var _ = require('underscore');
tree.Ruleset = function Ruleset(selectors, rules) {
this.selectors = selectors;
@@ -144,13 +145,15 @@ tree.Ruleset.prototype = {
}
}
- var rules = [];
+ var rule, rules = [], subrules;
for (i = 0; i < this.rules.length; i++) {
- var rule = this.rules[i];
+ rule = this.rules[i];
// Recursively flatten any nested rulesets
if (rule instanceof tree.Ruleset) {
rule.flatten(result, selectors, env);
+ } else if (rule instanceof tree.Subset) {
+ rule.collectRules(rules);
} else if (rule instanceof tree.Rule) {
rules.push(rule);
} else if (rule instanceof tree.Invalid) {
diff --git a/lib/carto/tree/subset.js b/lib/carto/tree/subset.js
new file mode 100644
index 000000000..c19ea3eeb
--- /dev/null
+++ b/lib/carto/tree/subset.js
@@ -0,0 +1,43 @@
+(function(tree) {
+
+tree.Subset = function Subset(symbolizer, pseudoelements, rules, subsets) {
+ if (symbolizer) {
+ var parts = symbolizer.split('/');
+ this.symbolizer = parts.pop();
+ this.instance = parts.length ? parts[0] : '__default__';
+ }
+ this.pseudoelements = pseudoelements || [];
+ this.rules = rules || [];
+ this.subsets = subsets || [];
+};
+
+tree.Subset.prototype.collectRules = function(rules) {
+ var start = rules.length;
+ for (i = 0; i < this.rules.length; i++) {
+ var rule = this.rules[i];
+
+ if (rule instanceof tree.Subset) {
+ rule.collectRules(rules);
+ } else if (rule instanceof tree.Rule) {
+ rules.push(rule);
+ }
+ }
+ for (i = start; i < rules.length; i++) {
+ var rule = rules[i];
+ if (this.pseudoelements.length) {
+ if (rule.pseudoelements.length) {
+ rule.pseudoelements = this.pseudoelements.concat(rule.pseudoelements);
+ }
+ else {
+ rule.pseudoelements = this.pseudoelements;
+ rule.pseudoname = this.pseudoelements[this.pseudoelements.length - 1].name;
+ }
+ }
+ if (this.symbolizer) {
+ rule.symbolizer = this.symbolizer;
+ rule.instance = this.instance;
+ }
+ }
+};
+
+})(require('../tree'));
diff --git a/test/rendering-mss/pseudo-element-inheritance.mss b/test/rendering-mss/pseudo-element-inheritance.mss
new file mode 100644
index 000000000..bcef1c657
--- /dev/null
+++ b/test/rendering-mss/pseudo-element-inheritance.mss
@@ -0,0 +1,41 @@
+#style {
+ text-face-name: "Deja Vu Sans";
+ text-name: [name];
+ text::layout {
+ content: [alt1];
+ dy: -10;
+ }
+ text::layout(1) {
+ content: [alt2];
+ dy: 10;
+ ::layout {
+ content: "test";
+ dy: 2;
+ }
+ }
+ [zoom>5] {
+ text::layout {
+ dy: -15;
+ }
+ text::layout(1) {
+ dy: 15;
+ dx: 10;
+ }
+ }
+ [zoom>5][zoom<10] {
+ text::layout::layout {
+ content: "test";
+ dx: 5;
+ }
+ }
+ [class=2] {
+ text::layout {
+ content: "Alt" + [alt1];
+ dy: 10;
+ }
+ text::layout(1)::layout {
+ dy: 3;
+ dx: 2;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/rendering-mss/pseudo-element-inheritance.xml b/test/rendering-mss/pseudo-element-inheritance.xml
new file mode 100644
index 000000000..08c19c01c
--- /dev/null
+++ b/test/rendering-mss/pseudo-element-inheritance.xml
@@ -0,0 +1,79 @@
+
\ No newline at end of file
diff --git a/test/rendering-mss/pseudo-element-nesting.mss b/test/rendering-mss/pseudo-element-nesting.mss
new file mode 100644
index 000000000..825459e3e
--- /dev/null
+++ b/test/rendering-mss/pseudo-element-nesting.mss
@@ -0,0 +1,24 @@
+#style {
+ text-face-name: "Deja Vu Sans";
+ text-name: [col1];
+ text::layout {
+ content: [col2];
+ dy: -10;
+ ::before {
+ content: 'before';
+ size: 10;
+ ::after {
+ content: 'after';
+ fill: red;
+ }
+ }
+ }
+ text::layout(1) {
+ content: [col3];
+ dy: 10;
+ }
+ text::layout(1)::before::after(2)::layout {
+ content: 'after layout';
+ dx: 3;
+ }
+}
\ No newline at end of file
diff --git a/test/rendering-mss/pseudo-element-nesting.xml b/test/rendering-mss/pseudo-element-nesting.xml
new file mode 100644
index 000000000..df5695e62
--- /dev/null
+++ b/test/rendering-mss/pseudo-element-nesting.xml
@@ -0,0 +1,24 @@
+
\ No newline at end of file
diff --git a/test/rendering-mss/pseudo-element-order.mss b/test/rendering-mss/pseudo-element-order.mss
new file mode 100644
index 000000000..4e8c71993
--- /dev/null
+++ b/test/rendering-mss/pseudo-element-order.mss
@@ -0,0 +1,31 @@
+#style {
+ text-face-name: "Deja Vu Sans";
+ text-name: 'root';
+ text::layout(1) {
+ content: 'layout1';
+ }
+ text::after(2) {
+ content: 'after2';
+ }
+ text::layout(3) {
+ content: 'layout3';
+ }
+ text::after(1) {
+ content: 'after1';
+ }
+ text::before(2) {
+ content: 'before2';
+ }
+ text::layout(0) {
+ content: 'layout0';
+ }
+ text::before(1) {
+ content: 'before1';
+ }
+ text::before(3) {
+ content: 'before3';
+ }
+ text::after {
+ content: 'after0';
+ }
+}
\ No newline at end of file
diff --git a/test/rendering-mss/pseudo-element-order.xml b/test/rendering-mss/pseudo-element-order.xml
new file mode 100644
index 000000000..672f55ddb
--- /dev/null
+++ b/test/rendering-mss/pseudo-element-order.xml
@@ -0,0 +1,16 @@
+
\ No newline at end of file