From 88a9d149abb4cec18eae9e8b31d007b32419735c Mon Sep 17 00:00:00 2001 From: Jordan Hollinger Date: Thu, 12 Jun 2014 17:09:51 -0400 Subject: [PATCH 1/3] Parse psuedo elements for rendering as child elements in symbolizer XML. This update reads information about symbolizer child elements from mapnik-reference. This information is used to create child elements in the XML, based on psuedo element syntax parsed from mss. Rendering of tags for symbolizers and child elements is similar, so the rendering is handled by a new shared object called RenderNode. This also handles indentation for nested child nodes. --- lib/carto/index.js | 7 +- lib/carto/parser.js | 34 ++++++- lib/carto/renderer.js | 1 + lib/carto/tree/definition.js | 69 +++---------- lib/carto/tree/pseudo.js | 8 ++ lib/carto/tree/reference.js | 144 +++++++++++++++++++++------ lib/carto/tree/rendernode.js | 184 +++++++++++++++++++++++++++++++++++ lib/carto/tree/rule.js | 48 +++++---- lib/carto/tree/ruleset.js | 7 +- lib/carto/tree/subset.js | 43 ++++++++ test/specificity.test.js | 1 + 11 files changed, 435 insertions(+), 111 deletions(-) create mode 100644 lib/carto/tree/pseudo.js create mode 100644 lib/carto/tree/rendernode.js create mode 100644 lib/carto/tree/subset.js 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/specificity.test.js b/test/specificity.test.js index 529b89f95..9dbb775cc 100644 --- a/test/specificity.test.js +++ b/test/specificity.test.js @@ -9,6 +9,7 @@ var helper = require('./support/helper'); function cleanupItem(key, value) { if (key === 'rules') return; else if (key === 'ruleIndex') return; + else if (key === 'subdefinitions') return; else if (key === 'elements') return value.map(function(item) { return item.value; }); else if (key === 'filters') { var arr = []; From f29038b404945fde92a3b781816589e522409c83 Mon Sep 17 00:00:00 2001 From: jhollinger2 Date: Mon, 23 Jun 2014 08:36:26 -0400 Subject: [PATCH 2/3] Add rendering tests for pseudo elements. Three primary test focuses: 1. Rendering nested pseudo elements and properly handling indentation 2. Ordering of the child elements in the symbolizer, based on order from mapnik-reference and indexes from mss syntax 3. Handling override and inheritance of child elements and their attributes from rule to rule --- .../pseudo-element-inheritance.mss | 41 ++++++++++ .../pseudo-element-inheritance.xml | 79 +++++++++++++++++++ test/rendering-mss/pseudo-element-nesting.mss | 24 ++++++ test/rendering-mss/pseudo-element-nesting.xml | 24 ++++++ test/rendering-mss/pseudo-element-order.mss | 31 ++++++++ test/rendering-mss/pseudo-element-order.xml | 16 ++++ 6 files changed, 215 insertions(+) create mode 100644 test/rendering-mss/pseudo-element-inheritance.mss create mode 100644 test/rendering-mss/pseudo-element-inheritance.xml create mode 100644 test/rendering-mss/pseudo-element-nesting.mss create mode 100644 test/rendering-mss/pseudo-element-nesting.xml create mode 100644 test/rendering-mss/pseudo-element-order.mss create mode 100644 test/rendering-mss/pseudo-element-order.xml 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 From d1304d409d765e8e4879c257cb77a672a8a4675c Mon Sep 17 00:00:00 2001 From: jhollinger2 Date: Mon, 23 Jun 2014 09:08:26 -0400 Subject: [PATCH 3/3] Remove unneeded change. --- test/specificity.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/specificity.test.js b/test/specificity.test.js index 9dbb775cc..529b89f95 100644 --- a/test/specificity.test.js +++ b/test/specificity.test.js @@ -9,7 +9,6 @@ var helper = require('./support/helper'); function cleanupItem(key, value) { if (key === 'rules') return; else if (key === 'ruleIndex') return; - else if (key === 'subdefinitions') return; else if (key === 'elements') return value.map(function(item) { return item.value; }); else if (key === 'filters') { var arr = [];