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