diff --git a/lib/mess/parser.js b/lib/mess/parser.js index 975f060..70847c6 100644 --- a/lib/mess/parser.js +++ b/lib/mess/parser.js @@ -769,7 +769,7 @@ mess.Parser = function Parser(env) { selector: function() { var a, attachment; var e, elements = []; - var f, filters = []; + var f, filters = {}; var z, zoom = tree.Zoom.all; var segments = 0, conditions = 0; @@ -786,7 +786,7 @@ mess.Parser = function Parser(env) { zoom &= z; conditions++; } else if (f) { - filters.push(f); + filters[f.id] = f; conditions++; } else if (attachment) { throw errorMessage('Encountered second attachment name', i - 1); diff --git a/lib/mess/renderer.js b/lib/mess/renderer.js index 5becf0a..283bc35 100644 --- a/lib/mess/renderer.js +++ b/lib/mess/renderer.js @@ -285,19 +285,19 @@ mess.Renderer = function Renderer(env) { var result = []; outer: for (var i = 0; i < selectors.length; i++) { for (var j = result.length - 1; j >= 0; j--) { - if (result[j].includesFiltersOf(selectors[i])) { + if (result[j].isIncludedIn(selectors[i])) { if (result[j].includesZoomOf(selectors[i])) { // The existing selector is already more generic than // this one. continue outer; } - if (selectors[i].includesFiltersOf(result[j])) { + if (selectors[i].isIncludedIn(result[j])) { // Merge selectors with the same filters but different zooms. result[j].zoom |= selectors[i].zoom; continue outer; } } - if (selectors[i].includesFiltersOf(result[j]) && + if (selectors[i].isIncludedIn(result[j]) && selectors[i].includesZoomOf(result[j])) { // This selector is more generic; remove the old one. result.splice(j, 1); @@ -335,7 +335,6 @@ mess.Renderer = function Renderer(env) { // // if (shortcut) return definitions; - for (var i = 0; i < definitions.length; i++) { if (!definitions[i].selector.sound()) { continue; @@ -345,7 +344,6 @@ mess.Renderer = function Renderer(env) { var selectors = [ definitions[i].selector.clone() ]; // Add the negated previous selectors. - var mergeTime = +new Date; for (var j = 0; j < previousSelectors.length; j++) { for (var k = selectors.length - 1; k >= 0; k--) { @@ -372,6 +370,7 @@ mess.Renderer = function Renderer(env) { for (var k = 0; k < selectors.length; k++) { var rule = definitions[i].clone(); rule.selector = selectors[k]; + tree.Filter.simplify(rule.selector.filters); rules.push(rule); } } @@ -429,7 +428,7 @@ mess.Renderer = function Renderer(env) { output = []; if (err) { - callback(err) + callback(err) return; } @@ -500,7 +499,7 @@ mess.Renderer = function Renderer(env) { + '+nadgrids=@null +no_defs">\n'); output.push(''); - + env.errors.map(function(e) { if (!e.line && e.index && e.filename) { var matches = stylesheets.filter(function(s) { diff --git a/lib/mess/tree/filter.js b/lib/mess/tree/filter.js index 0135523..bb4f9e8 100644 --- a/lib/mess/tree/filter.js +++ b/lib/mess/tree/filter.js @@ -5,6 +5,7 @@ tree.Filter = function Filter(key, op, val, index) { this.op = op; this.val = val; this.index = index; + this.id = this.key + this.op + this.val; }; tree.Filter.prototype.toXML = function(env) { @@ -21,7 +22,7 @@ tree.Filter.prototype.toXML = function(env) { }; tree.Filter.prototype.toString = function() { - return '[' + this.key + ' ' + this.op.value + ' ' + this.val + ']'; + return '[' + this.id + ']'; }; /** @@ -60,16 +61,107 @@ tree.Filter.prototype.clone = function() { }; tree.Filter.sound = function(filters) { - // shortcut for single-filter filters - if (filters.length == 1) return true; - for (var i = 0; i < filters.length; i++) { - for (var j = i + 1; j < filters.length; j++) { - if (filters[i].conflictsWith(filters[j])) { - return false; - } + // Shortcut for single-filter filters. + if (Object.keys(filters).length == 1) return true; + + var byKey = {}; + var filter, key, value; + + for (var id in filters) { + filter = filters[id]; + key = filters[id].key; + value = filters[id].val.toString(); + + if (!(key in byKey)) { + byKey[key] = {}; + } + + switch (filter.op.value) { + case '=': + if ('=' in byKey[key] && byKey[key]['='] != value) return false; + if ('!=' in byKey[key] && byKey[key]['!='].indexOf(value) >= 0) return false; + if ('>' in byKey[key] && byKey[key]['>'] <= value) return false; + if ('<' in byKey[key] && byKey[key]['<'] >= value) return false; + if ('>=' in byKey[key] && byKey[key]['>='] < value) return false; + if ('<=' in byKey[key] && byKey[key]['<='] > value) return false; + byKey[key]['='] = value; + break; + + case '!=': + if ('=' in byKey[key] && byKey[key]['='] == value) return false; + if (!('!=' in byKey[key])) byKey[key]['!='] = []; + byKey[key]['!='].push(value); + break; + + case '>': + if ('=' in byKey[key] && byKey[key]['='] <= value) return false; + if ('<' in byKey[key] && byKey[key]['<'] <= value) return false; + if ('<=' in byKey[key] && byKey[key]['<='] <= value) return false; + byKey[key]['>'] = value; + break; + + case '<': + if ('=' in byKey[key] && byKey[key]['='] >= value) return false; + if ('>' in byKey[key] && byKey[key]['<'] >= value) return false; + if ('>=' in byKey[key] && byKey[key]['<='] >= value) return false; + byKey[key]['<'] = value; + break; + + case '>=': + if ('=' in byKey[key] && byKey[key]['='] < value) return false; + if ('<' in byKey[key] && byKey[key]['<'] < value) return false; + if ('<=' in byKey[key] && byKey[key]['<='] < value) return false; + byKey[key]['>='] = value; + break; + + case '<=': + if ('=' in byKey[key] && byKey[key]['='] > value) return false; + if ('<' in byKey[key] && byKey[key]['<'] > value) return false; + if ('<=' in byKey[key] && byKey[key]['<='] > value) return false; + byKey[key]['<='] = value; + break; } } + delete byKey; return true; }; +tree.Filter.simplify = function(filters) { + // Shortcut for single-filter filters. + if (Object.keys(filters).length == 1) return true; + + var byKey = {}; + var filter, key, value; + + for (var id in filters) { + filter = filters[id]; + key = filters[id].key; + value = filters[id].val.toString(); + + switch (filter.op.value) { + case '=': + if (byKey[key]) { + for (var i in byKey[key]) { + delete filters[i]; + } + } + byKey[key] = '='; + break; + + + case '!=': + if (byKey[key] == '=') { + delete filters[id]; + } else { + if (!byKey[key]) byKey[key] = {}; + byKey[key][id] = true; + } + break; + + // TODO: >, <, >=, <= + } + } + delete byKey; +}; + })(require('mess/tree')); diff --git a/lib/mess/tree/selector.js b/lib/mess/tree/selector.js index a3251d4..4b8b411 100644 --- a/lib/mess/tree/selector.js +++ b/lib/mess/tree/selector.js @@ -5,7 +5,7 @@ var assert = require('assert'); tree.Selector = function Selector(elements, filters, zoom, attachment, conditions, index) { this.elements = elements || []; this.attachment = attachment || '__default__'; - this.filters = filters || []; + this.filters = filters; this.zoom = zoom; this.conditions = conditions; this.index = index; @@ -19,23 +19,32 @@ tree.Selector.prototype.debug = function() { var str = "[" + num + "] " + this.elements.join(''); if (this.attachment !== '__default__') str += '::' + this.attachment; - str += ': '; - str += tree.Zoom.toString(this.zoom); - str += ' ' + this.filters.join(' '); + str += ': Zoom[' + tree.Zoom.toString(this.zoom) + '] '; + for (var id in this.filters) { + str += this.filters[id] + ' '; + } return str; }; +tree.Selector.prototype.hash = function() { + var filters = Object.keys(this.filters); + filters.sort(); + return (this.zoom & tree.Zoom.all) + '[' + filters.join('][') + ']'; +}; + tree.Selector.prototype.sound = function() { if (!this.zoom) return false; - if (this.filters.length && !tree.Filter.sound(this.filters)) return false; - return true; + return tree.Filter.sound(this.filters); }; tree.Selector.prototype.clone = function() { var obj = Object.create(Object.getPrototypeOf(this)); obj.elements = this.elements.slice(); obj.attachment = this.attachment; - obj.filters = this.filters.slice(); + obj.filters = {}; + for (var id in this.filters) { + obj.filters[id] = this.filters[id]; + } obj.zoom = this.zoom; obj.conditions = this.conditions; obj.index = this.index; @@ -44,7 +53,9 @@ tree.Selector.prototype.clone = function() { tree.Selector.prototype.merge = function(obj) { Array.prototype.push.apply(this.elements, obj.elements); - Array.prototype.push.apply(this.filters, obj.filters); + for (var id in obj.filters) { + this.filters[id] = obj.filters[id]; + } if (obj.attachment) this.attachment = obj.attachment; this.index = obj.index; this.zoom &= obj.zoom; @@ -58,100 +69,67 @@ tree.Selector.prototype.merge = function(obj) { * it splits up the selector and creates one for each condition. */ tree.Selector.prototype.mergeOrConditions = function(obj) { - var result = [ this ]; + var result = {}; - if (obj.filters.length) { + if (Object.keys(obj.filters).length) { if (obj.zoom != tree.Zoom.all) { + var clone = this.clone(); + clone.zoom &= obj.zoom; + if (clone.sound()) result[clone.hash()] = clone; + var negatedZoom = ~obj.zoom; - for (var i = 0; i < obj.filters.length; i++) { + for (var id in obj.filters) { var selector = this.clone(); - selector.filters.push(obj.filters[i]); + selector.filters[id] = obj.filters[id]; selector.zoom &= negatedZoom; - result.push(selector); + if (selector.sound()) { + result[selector.hash()] = selector; + } } } else { // No zoom conditions, just split the selectors for each filter in obj. - for (var i = 1; i < obj.filters.length; i++) { + for (var id in obj.filters) { var selector = this.clone(); - selector.filters.push(obj.filters[i]); - result.push(selector); + selector.filters[id] = obj.filters[id]; + if (selector.sound()) { + result[selector.hash()] = selector; + } } - this.filters.push(obj.filters[0]); } + } else { + var clone = this.clone(); + clone.zoom &= obj.zoom; + if (clone.sound()) result[clone.hash()] = clone; } - this.zoom &= obj.zoom; - // Simplify the resulting sound selectors. - result = result.filter(function(selector) { - if (selector.sound()) { - selector.simplifyFilters(); - return true; - } - }); - - - // Check selectors for soundness before we return them. - return result; + var arr = []; + for (var id in result) arr.push(result[id]); + return arr; }; -tree.Selector.prototype.simplifyFilters = function() { - var simplified = []; - var a, b; - filters: for (var i = 0; i < this.filters.length; i++) { - a = this.filters[i]; - // Operate from the back so that we don't run into renumbering problems - // when deleting items from the array. - for (var j = simplified.length - 1; j >= 0; j--) { - b = simplified[j]; - if (b.key === a.key && - (b.op.value === '=' || - (b.op.value == a.op.value && b.val.value == a.val.value) - )) { - continue filters; - } - else if (a.op.value === '=' && - a.key === b.key) { - simplified.splice(j, 1); - } - } - simplified.push(a); - } - this.filters = simplified; -}; - -tree.Selector.prototype.includesFiltersOf = function(other) { - // Check that this is a strict subset of other. - var a, b; - outer: for (var i = 0; i < this.filters.length; i++) { - a = this.filters[i]; - for (var j = 0; j < other.filters.length; j++) { - b = other.filters[j]; - if (a.key === b.key && - a.op.value == b.op.value && - a.val.value == b.val.value) { - continue outer; - } - } - // Couldn't find the filter in other. - return false; +tree.Selector.prototype.isIncludedIn = function(other) { + for (var id in this.filters) { + if (!(id in other.filters)) return false; } return true; }; tree.Selector.prototype.includesZoomOf = function(other) { - return (this.zoom | other.zoom) == this.zoom; + return (this.zoom | other.zoom) == other.zoom; }; tree.Selector.prototype.negate = function() { var obj = Object.create(Object.getPrototypeOf(this)); obj.elements = this.elements.slice(); obj.attachment = this.attachment; - obj.index = this.index; - - obj.filters = this.filters.map(function(filter) { - return filter.negate(); - }); + obj.filters = {}; + for (var id in this.filters) { + var negated = this.filters[id].negate(); + obj.filters[negated.id] = negated; + } obj.zoom = ~this.zoom; + obj.conditions = this.conditions + obj.index = this.index; return obj; }; @@ -194,9 +172,11 @@ tree.Selector.prototype.layers = function(env) { tree.Selector.prototype.combinedFilter = function(env) { var conditions = tree.Zoom.toXML(this.zoom); - var filters = this.filters.map(function(filter) { - return '(' + filter.toXML(env).trim() + ')'; - }); + var filters = []; + for (var id in this.filters) { + filters.push('(' + this.filters[id].toXML(env).trim() + ')'); + } + if (filters.length) { conditions.push('' + filters.join(' and ') + ''); } diff --git a/test/rendering.test.js b/test/rendering.test.js index d373b70..486f73f 100644 --- a/test/rendering.test.js +++ b/test/rendering.test.js @@ -44,8 +44,8 @@ helper.files('rendering', 'mml', function(file) { beforeExit(function() { if (!completed && renderResult) { - // console.warn(helper.stylize('renderer produced:', 'bold')); - // console.warn(renderResult); + console.warn(helper.stylize('renderer produced:', 'bold')); + console.warn(renderResult); } assert.ok(completed, 'Rendering finished.'); }); diff --git a/test/rendering/complex_cascades.result b/test/rendering/complex_cascades.result index e8614bd..48a0728 100644 --- a/test/rendering/complex_cascades.result +++ b/test/rendering/complex_cascades.result @@ -10,7 +10,12 @@ - + 12500000 + ([NAME] = 'Canada') + + + + 12500000 ([NAME] = 'Canada') diff --git a/test/specificity.test.js b/test/specificity.test.js index fed52d8..360f48b 100644 --- a/test/specificity.test.js +++ b/test/specificity.test.js @@ -10,11 +10,13 @@ var helper = require('./support/helper'); function cleanupItem(key, value) { if (!key) return value.map(function(item) { return item.selector }); else if (key === 'elements') return value.map(function(item) { return item.value; }); - else if (key === 'filters' && !value.length) return undefined; + else if (key === 'filters') { + if (Object.keys(value).length) return Object.keys(value); + } else if (key === 'attachment' && value === '__default__') return undefined; else if (key === 'index') return undefined; else if (key === 'zoom') { - if (value == tree.Zoom.all) return void null; + if (value == tree.Zoom.all) return undefined; else return tree.Zoom.toString(value); } else if (key === 'op') return value.value; diff --git a/test/specificity/demo.result b/test/specificity/demo.result index 3b318f7..2ab94e5 100644 --- a/test/specificity/demo.result +++ b/test/specificity/demo.result @@ -1,8 +1,8 @@ [ {"elements":["#countries",".foo",".bar",".baz"]}, {"elements":["#countries",".countries",".two"]}, - {"elements":["#world"],"filters":[{"key":"NAME","op":"=","val":"United States"},{"key":"BLUE","op":"=","val":"red"}],"conditions":2}, - {"elements":["#world"],"filters":[{"key":"NAME","op":"=","val":"United States"}],"conditions":1}, + {"elements":["#world"],"filters":["NAME=United States","BLUE=red"],"conditions":2}, + {"elements":["#world"],"filters":["NAME=United States"],"conditions":1}, {"elements":["#countries"]}, {"elements":["#countries"]}, {"elements":["#world"]}, diff --git a/test/specificity/filters_and_ids.result b/test/specificity/filters_and_ids.result index 3d2b02b..bdf4ef2 100644 --- a/test/specificity/filters_and_ids.result +++ b/test/specificity/filters_and_ids.result @@ -1,8 +1,8 @@ [ - {"elements":["#world","#countries"],"filters":[{"key":"NAME","op":"=","val":"United States"}],"conditions":1}, - {"elements":["#world"],"filters":[{"key":"NAME","op":"=","val":"United States"}],"zoom":"......XXXXXXXXXXXXXXXXX","conditions":2}, - {"elements":["#world"],"filters":[{"key":"NAME","op":"=","val":"United States"}],"conditions":1}, - {"elements":["#world"],"filters":[{"key":"NAME","op":"=","val":"Canada"}],"conditions":1}, + {"elements":["#world","#countries"],"filters":["NAME=United States"],"conditions":1}, + {"elements":["#world"],"filters":["NAME=United States"],"zoom":"......XXXXXXXXXXXXXXXXX","conditions":2}, + {"elements":["#world"],"filters":["NAME=United States"],"conditions":1}, + {"elements":["#world"],"filters":["NAME=Canada"],"conditions":1}, {"elements":[],"zoom":"......XXXXXXXXXXXXXXXXX","conditions":1}, - {"elements":[],"filters":[{"key":"NAME","op":"=","val":"United States"}],"conditions":1} + {"elements":[],"filters":["NAME=United States"],"conditions":1} ] \ No newline at end of file