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 + '\n'; - } else { - xml += '>\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 += '\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