diff --git a/src/epicker.html b/src/epicker.html
index f3e038d33..fc1f34e52 100644
--- a/src/epicker.html
+++ b/src/epicker.html
@@ -63,7 +63,10 @@ section > div:first-child {
margin: 0;
position: relative;
}
-section > div > textarea {
+section.invalidFilter > div:first-child {
+ border-color: red;
+}
+section > div:first-child > textarea {
background-color: #fff;
border: none;
box-sizing: border-box;
@@ -75,10 +78,7 @@ section > div > textarea {
resize: none;
width: 100%;
}
-section > div > textarea.invalidFilter {
- background-color: #fee;
-}
-section > div > textarea + div {
+section > div:first-child > textarea + div {
background-color: #aaa;
bottom: 0;
color: white;
@@ -86,6 +86,9 @@ section > div > textarea + div {
position: absolute;
right: 0;
}
+section.invalidFilter > div:first-child > textarea + div {
+ background-color: red;
+}
section > div:first-child + div {
direction: ltr;
margin: 2px 0;
diff --git a/src/js/contentscript.js b/src/js/contentscript.js
index 91ef518a9..33f241aca 100644
--- a/src/js/contentscript.js
+++ b/src/js/contentscript.js
@@ -137,19 +137,9 @@ vAPI.domFilterer = (function() {
/******************************************************************************/
-var jobQueue = [
- { t: 'css-hide', _0: [] }, // to inject in style tag
- { t: 'css-style', _0: [] }, // to inject in style tag
- { t: 'css-ssel', _0: [] }, // to manually hide (incremental)
- { t: 'css-csel', _0: [] } // to manually hide (not incremental)
-];
-
-var reParserEx = /:(?:has|matches-css|matches-css-before|matches-css-after|style|xpath)\(.+?\)$/;
-
var allExceptions = createSet(),
allSelectors = createSet(),
- stagedNodes = [],
- matchesProp = vAPI.matchesProp;
+ stagedNodes = [];
// Complex selectors, due to their nature may need to be "de-committed". A
// Set() is used to implement this functionality.
@@ -308,100 +298,179 @@ var platformHideNode = vAPI.hideNode,
/******************************************************************************/
-var runSimpleSelectorJob = function(job, root, fn) {
- if ( job._1 === undefined ) {
- job._1 = job._0.join(cssNotHiddenId + ',');
- }
- if ( root[matchesProp](job._1) ) {
- fn(root);
- }
- var nodes = root.querySelectorAll(job._1),
- i = nodes.length;
- while ( i-- ) {
- fn(nodes[i], job);
- }
-};
+// 'P' stands for 'Procedural'
-var runComplexSelectorJob = function(job, fn) {
- if ( job._1 === undefined ) {
- job._1 = job._0.join(',');
- }
- var nodes = document.querySelectorAll(job._1),
- i = nodes.length;
- while ( i-- ) {
- fn(nodes[i], job);
- }
+var PSelectorHasTask = function(task) {
+ this.selector = task[1];
};
-
-var runHasJob = function(job, fn) {
- var nodes = document.querySelectorAll(job._0),
- i = nodes.length, node;
- while ( i-- ) {
- node = nodes[i];
- if ( node.querySelector(job._1) !== null ) {
- fn(node, job);
+PSelectorHasTask.prototype.exec = function(input) {
+ var output = [];
+ for ( var i = 0, n = input.length; i < n; i++ ) {
+ if ( input[i].querySelector(this.selector) !== null ) {
+ output.push(input[i]);
}
}
+ return output;
};
-// '/' = ascii 0x2F */
-
-var parseMatchesCSSJob = function(raw) {
- var prop = raw.trim();
- if ( prop === '' ) { return null; }
- var pos = prop.indexOf(':'),
- v = pos !== -1 ? prop.slice(pos + 1).trim() : '',
- vlen = v.length;
- if (
- vlen > 1 &&
- v.charCodeAt(0) === 0x2F &&
- v.charCodeAt(vlen-1) === 0x2F
- ) {
- try { v = new RegExp(v.slice(1, -1)); } catch(ex) { return null; }
- }
- return { k: prop.slice(0, pos).trim(), v: v };
+var PSelectorHasTextTask = function(task) {
+ this.needle = new RegExp(task[1]);
};
-
-var runMatchesCSSJob = function(job, fn) {
- var nodes = document.querySelectorAll(job._0),
- i = nodes.length;
- if ( i === 0 ) { return; }
- if ( typeof job._1 === 'string' ) {
- job._1 = parseMatchesCSSJob(job._1);
- }
- if ( job._1 === null ) { return; }
- var k = job._1.k,
- v = job._1.v,
- node, style, match;
- while ( i-- ) {
- node = nodes[i];
- style = window.getComputedStyle(node, job._2);
- if ( style === null ) { continue; } /* FF */
- if ( v instanceof RegExp ) {
- match = v.test(style[k]);
- } else {
- match = style[k] === v;
- }
- if ( match ) {
- fn(node, job);
+PSelectorHasTextTask.prototype.exec = function(input) {
+ var output = [];
+ for ( var i = 0, n = input.length; i < n; i++ ) {
+ if ( this.needle.test(input[i].textContent) ) {
+ output.push(input[i]);
}
}
+ return output;
};
-var runXpathJob = function(job, fn) {
- if ( job._1 === undefined ) {
- job._1 = document.createExpression(job._0, null);
+var PSelectorIfTask = function(task) {
+ this.pselector = new PSelector(task[1]);
+};
+PSelectorIfTask.prototype.target = true;
+PSelectorIfTask.prototype.exec = function(input) {
+ var output = [];
+ for ( var i = 0, n = input.length; i < n; i++ ) {
+ if ( this.pselector.test(input[i]) === this.target ) {
+ output.push(input[i]);
+ }
}
- var xpr = job._2 = job._1.evaluate(
- document,
- XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
- job._2 || null
- );
- var i = xpr.snapshotLength, node;
+ return output;
+};
+
+var PSelectorIfNotTask = function(task) {
+ PSelectorIfTask.call(this, task);
+ this.target = false;
+};
+PSelectorIfNotTask.prototype = Object.create(PSelectorIfTask.prototype);
+PSelectorIfNotTask.prototype.constructor = PSelectorIfNotTask;
+
+var PSelectorMatchesCSSTask = function(task) {
+ this.name = task[1].name;
+ this.value = new RegExp(task[1].value);
+};
+PSelectorMatchesCSSTask.prototype.pseudo = null;
+PSelectorMatchesCSSTask.prototype.exec = function(input) {
+ var output = [], style;
+ for ( var i = 0, n = input.length; i < n; i++ ) {
+ style = window.getComputedStyle(input[i], this.pseudo);
+ if ( style === null ) { return null; } /* FF */
+ if ( this.value.test(style[this.name]) ) {
+ output.push(input[i]);
+ }
+ }
+ return output;
+};
+
+var PSelectorMatchesCSSAfterTask = function(task) {
+ PSelectorMatchesCSSTask.call(this, task);
+ this.pseudo = ':after';
+};
+PSelectorMatchesCSSAfterTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype);
+PSelectorMatchesCSSAfterTask.prototype.constructor = PSelectorMatchesCSSAfterTask;
+
+var PSelectorMatchesCSSBeforeTask = function(task) {
+ PSelectorMatchesCSSTask.call(this, task);
+ this.pseudo = ':before';
+};
+PSelectorMatchesCSSBeforeTask.prototype = Object.create(PSelectorMatchesCSSTask.prototype);
+PSelectorMatchesCSSBeforeTask.prototype.constructor = PSelectorMatchesCSSBeforeTask;
+
+var PSelectorXpathTask = function(task) {
+ this.xpe = document.createExpression(task[1], null);
+ this.xpr = null;
+};
+PSelectorXpathTask.prototype.exec = function(input) {
+ var output = [], j, node;
+ for ( var i = 0, n = input.length; i < n; i++ ) {
+ this.xpr = this.xpe.evaluate(
+ input[i],
+ XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
+ this.xpr
+ );
+ j = this.xpr.snapshotLength;
+ while ( j-- ) {
+ node = this.xpr.snapshotItem(j);
+ if ( node.nodeType === 1 ) {
+ output.push(node);
+ }
+ }
+ }
+ return output;
+};
+
+var PSelector = function(o) {
+ if ( PSelector.prototype.operatorToTaskMap === undefined ) {
+ PSelector.prototype.operatorToTaskMap = new Map([
+ [ ':has', PSelectorHasTask ],
+ [ ':has-text', PSelectorHasTextTask ],
+ [ ':if', PSelectorIfTask ],
+ [ ':if-not', PSelectorIfNotTask ],
+ [ ':matches-css', PSelectorMatchesCSSTask ],
+ [ ':matches-css-after', PSelectorMatchesCSSAfterTask ],
+ [ ':matches-css-before', PSelectorMatchesCSSBeforeTask ],
+ [ ':xpath', PSelectorXpathTask ]
+ ]);
+ }
+ this.raw = o.raw;
+ this.selector = o.selector;
+ this.tasks = [];
+ var tasks = o.tasks, task, ctor;
+ for ( var i = 0; i < tasks.length; i++ ) {
+ task = tasks[i];
+ ctor = this.operatorToTaskMap.get(task[0]);
+ this.tasks.push(new ctor(task));
+ }
+};
+PSelector.prototype.operatorToTaskMap = undefined;
+PSelector.prototype.prime = function(input) {
+ var root = input || document;
+ if ( this.selector !== '' ) {
+ return root.querySelectorAll(this.selector);
+ }
+ return [ root ];
+};
+PSelector.prototype.exec = function(input) {
+ //var t0 = window.performance.now();
+ var tasks = this.tasks, nodes = this.prime(input);
+ for ( var i = 0, n = tasks.length; i < n && nodes.length !== 0; i++ ) {
+ nodes = tasks[i].exec(nodes);
+ }
+ //console.log('%s: %s ms', this.raw, (window.performance.now() - t0).toFixed(2));
+ return nodes;
+};
+PSelector.prototype.test = function(input) {
+ //var t0 = window.performance.now();
+ var tasks = this.tasks, nodes = this.prime(input), aa0 = [ null ], aa;
+ for ( var i = 0, ni = nodes.length; i < ni; i++ ) {
+ aa0[0] = nodes[i]; aa = aa0;
+ for ( var j = 0, nj = tasks.length; j < nj && aa.length !== 0; j++ ) {
+ aa = tasks[i].exec(aa);
+ }
+ if ( aa.length !== 0 ) { return true; }
+ }
+ //console.log('%s: %s ms', this.raw, (window.performance.now() - t0).toFixed(2));
+ return false;
+};
+
+var PSelectors = function() {
+ this.entries = [];
+};
+PSelectors.prototype.add = function(o) {
+ this.entries.push(new PSelector(o));
+};
+PSelectors.prototype.forEachNode = function(callback) {
+ var pfilters = this.entries,
+ i = pfilters.length,
+ pfilter, nodes, j;
while ( i-- ) {
- node = xpr.snapshotItem(i);
- if ( node.nodeType === 1 ) {
- fn(node, job);
+ pfilter = pfilters[i];
+ nodes = pfilter.exec();
+ j = nodes.length;
+ while ( j-- ) {
+ callback(nodes[j], pfilter);
}
}
};
@@ -418,14 +487,52 @@ var domFilterer = {
hiddenNodeCount: 0,
hiddenNodeEnforcer: false,
loggerEnabled: undefined,
- styleTags: [],
- jobQueue: jobQueue,
- // Stock jobs.
- job0: jobQueue[0],
- job1: jobQueue[1],
- job2: jobQueue[2],
- job3: jobQueue[3],
+ newHideSelectorBuffer: [], // Hide style filter buffer
+ newStyleRuleBuffer: [], // Non-hide style filter buffer
+ simpleHideSelectors: { // Hiding filters: simple selectors
+ entries: [],
+ matchesProp: vAPI.matchesProp,
+ selector: undefined,
+ add: function(selector) {
+ this.entries.push(selector);
+ this.selector = undefined;
+ },
+ forEachNodeOfSelector: function(/*callback, root, extra*/) {
+ },
+ forEachNode: function(callback, root, extra) {
+ if ( this.selector === undefined ) {
+ this.selector = this.entries.join(extra + ',') + extra;
+ }
+ if ( root[this.matchesProp](this.selector) ) {
+ callback(root);
+ }
+ var nodes = root.querySelectorAll(this.selector),
+ i = nodes.length;
+ while ( i-- ) {
+ callback(nodes[i]);
+ }
+ }
+ },
+ complexHideSelectors: { // Hiding filters: complex selectors
+ entries: [],
+ selector: undefined,
+ add: function(selector) {
+ this.entries.push(selector);
+ this.selector = undefined;
+ },
+ forEachNode: function(callback) {
+ if ( this.selector === undefined ) {
+ this.selector = this.entries.join(',');
+ }
+ var nodes = document.querySelectorAll(this.selector),
+ i = nodes.length;
+ while ( i-- ) {
+ callback(nodes[i]);
+ }
+ }
+ },
+ proceduralSelectors: new PSelectors(), // Hiding filters: procedural
addExceptions: function(aa) {
for ( var i = 0, n = aa.length; i < n; i++ ) {
@@ -433,58 +540,28 @@ var domFilterer = {
}
},
- // Job:
- // Stock jobs in job queue:
- // 0 = css rules/css declaration to remove visibility
- // 1 = css rules/any css declaration
- // 2 = simple css selectors/hide
- // 3 = complex css selectors/hide
- // Custom jobs:
- // matches-css/hide
- // has/hide
- // xpath/hide
-
- addSelector: function(s) {
- if ( allSelectors.has(s) || allExceptions.has(s) ) {
+ addSelector: function(selector) {
+ if ( allSelectors.has(selector) || allExceptions.has(selector) ) {
return;
}
- allSelectors.add(s);
- var sel0 = s, sel1 = '';
- if ( s.charCodeAt(s.length - 1) === 0x29 ) {
- var parts = reParserEx.exec(s);
- if ( parts !== null ) {
- sel1 = parts[0];
- }
- }
- if ( sel1 === '' ) {
- this.job0._0.push(sel0);
- if ( sel0.indexOf(' ') === -1 ) {
- this.job2._0.push(sel0);
- this.job2._1 = undefined;
+ allSelectors.add(selector);
+ if ( selector.charCodeAt(0) !== 0x7B /* '{' */ ) {
+ this.newHideSelectorBuffer.push(selector);
+ if ( selector.indexOf(' ') === -1 ) {
+ this.simpleHideSelectors.add(selector);
} else {
- this.job3._0.push(sel0);
- this.job3._1 = undefined;
+ this.complexHideSelectors.add(selector);
}
return;
}
- sel0 = sel0.slice(0, sel0.length - sel1.length);
- if ( sel1.lastIndexOf(':has', 0) === 0 ) {
- this.jobQueue.push({ t: 'has-hide', raw: s, _0: sel0, _1: sel1.slice(5, -1) });
- } else if ( sel1.lastIndexOf(':matches-css', 0) === 0 ) {
- if ( sel1.lastIndexOf(':matches-css-before', 0) === 0 ) {
- this.jobQueue.push({ t: 'matches-css-hide', raw: s, _0: sel0, _1: sel1.slice(20, -1), _2: ':before' });
- } else if ( sel1.lastIndexOf(':matches-css-after', 0) === 0 ) {
- this.jobQueue.push({ t: 'matches-css-hide', raw: s, _0: sel0, _1: sel1.slice(19, -1), _2: ':after' });
- } else {
- this.jobQueue.push({ t: 'matches-css-hide', raw: s, _0: sel0, _1: sel1.slice(13, -1), _2: null });
- }
- } else if ( sel1.lastIndexOf(':style', 0) === 0 ) {
- this.job1._0.push(sel0 + ' { ' + sel1.slice(7, -1) + ' }');
- this.job1._1 = undefined;
- } else if ( sel1.lastIndexOf(':xpath', 0) === 0 ) {
- this.jobQueue.push({ t: 'xpath-hide', raw: s, _0: sel1.slice(7, -1) });
+ var o = JSON.parse(selector);
+ if ( o.style ) {
+ this.newStyleRuleBuffer.push(o.parts.join(' '));
+ return;
+ }
+ if ( o.procedural ) {
+ this.proceduralSelectors.add(o);
}
- return;
},
addSelectors: function(aa) {
@@ -497,27 +574,27 @@ var domFilterer = {
this.commitTimer.clear();
var beforeHiddenNodeCount = this.hiddenNodeCount,
- styleText = '', i, n;
+ styleText = '', i;
- // Stock job 0 = css rules/hide
- if ( this.job0._0.length ) {
- styleText = '\n:root ' + this.job0._0.join(',\n:root ') + '\n{ display: none !important; }';
- this.job0._0.length = 0;
+ // CSS rules/hide
+ if ( this.newHideSelectorBuffer.length ) {
+ styleText = '\n:root ' + this.newHideSelectorBuffer.join(',\n:root ') + '\n{ display: none !important; }';
+ this.newHideSelectorBuffer.length = 0;
}
- // Stock job 1 = css rules/any css declaration
- if ( this.job1._0.length ) {
- styleText += '\n' + this.job1._0.join('\n');
- this.job1._0.length = 0;
+ // CSS rules/any css declaration
+ if ( this.newStyleRuleBuffer.length ) {
+ styleText += '\n' + this.newStyleRuleBuffer.join('\n');
+ this.newStyleRuleBuffer.length = 0;
}
// Simple selectors: incremental.
- // Stock job 2 = simple css selectors/hide
- if ( this.job2._0.length ) {
+ // Simple css selectors/hide
+ if ( this.simpleHideSelectors.entries.length ) {
i = stagedNodes.length;
while ( i-- ) {
- runSimpleSelectorJob(this.job2, stagedNodes[i], hideNode);
+ this.simpleHideSelectors.forEachNode(hideNode, stagedNodes[i], cssNotHiddenId);
}
}
stagedNodes = [];
@@ -526,17 +603,16 @@ var domFilterer = {
complexSelectorsOldResultSet = complexSelectorsCurrentResultSet;
complexSelectorsCurrentResultSet = createSet('object');
- // Stock job 3 = complex css selectors/hide
+ // Complex css selectors/hide
// The handling of these can be considered optional, since they are
// also applied declaratively using a style tag.
- if ( this.job3._0.length ) {
- runComplexSelectorJob(this.job3, complexHideNode);
+ if ( this.complexHideSelectors.entries.length ) {
+ this.complexHideSelectors.forEachNode(complexHideNode);
}
- // Custom jobs. No optional since they can't be applied in a
- // declarative way.
- for ( i = 4, n = this.jobQueue.length; i < n; i++ ) {
- this.runJob(this.jobQueue[i], complexHideNode);
+ // Procedural cosmetic filters
+ if ( this.proceduralSelectors.entries.length ) {
+ this.proceduralSelectors.forEachNode(complexHideNode);
}
// https://github.com/gorhill/uBlock/issues/1912
@@ -595,6 +671,10 @@ var domFilterer = {
this.commitTimer.start();
},
+ createProceduralFilter: function(o) {
+ return new PSelector(o);
+ },
+
getExcludeId: function() {
if ( this.excludeId === undefined ) {
this.excludeId = vAPI.randomToken();
@@ -616,20 +696,6 @@ var domFilterer = {
this.commitTimer = new vAPI.SafeAnimationFrame(this.commit_.bind(this));
},
- runJob: function(job, fn) {
- switch ( job.t ) {
- case 'has-hide':
- runHasJob(job, fn);
- break;
- case 'matches-css-hide':
- runMatchesCSSJob(job, fn);
- break;
- case 'xpath-hide':
- runXpathJob(job, fn);
- break;
- }
- },
-
showNode: function(node) {
node.hidden = false;
platformUnhideNode(node);
@@ -1248,14 +1314,14 @@ vAPI.domSurveyor = (function() {
// Need to do this before committing DOM filterer, as needed info
// will no longer be there after commit.
- if ( firstSurvey || domFilterer.job0._0.length ) {
+ if ( firstSurvey || domFilterer.newHideSelectorBuffer.length ) {
messaging.send(
'contentscript',
{
what: 'cosmeticFiltersInjected',
type: 'cosmetic',
hostname: window.location.hostname,
- selectors: domFilterer.job0._0,
+ selectors: domFilterer.newHideSelectorBuffer,
first: firstSurvey,
cost: surveyCost
}
@@ -1263,7 +1329,7 @@ vAPI.domSurveyor = (function() {
}
// Shutdown surveyor if too many consecutive empty resultsets.
- if ( domFilterer.job0._0.length === 0 ) {
+ if ( domFilterer.newHideSelectorBuffer.length === 0 ) {
cosmeticSurveyingMissCount += 1;
} else {
cosmeticSurveyingMissCount = 0;
diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js
index ebfdc7392..6d693adb5 100644
--- a/src/js/cosmetic-filtering.js
+++ b/src/js/cosmetic-filtering.js
@@ -39,6 +39,35 @@ var µb = µBlock;
var encode = JSON.stringify;
var decode = JSON.parse;
+var isValidCSSSelector = (function() {
+ var div = document.createElement('div'),
+ matchesFn;
+ // Keep in mind:
+ // https://github.com/gorhill/uBlock/issues/693
+ // https://github.com/gorhill/uBlock/issues/1955
+ if ( div.matches instanceof Function ) {
+ matchesFn = div.matches.bind(div);
+ } else if ( div.mozMatchesSelector instanceof Function ) {
+ matchesFn = div.mozMatchesSelector.bind(div);
+ } else if ( div.webkitMatchesSelector instanceof Function ) {
+ matchesFn = div.webkitMatchesSelector.bind(div);
+ } else if ( div.msMatchesSelector instanceof Function ) {
+ matchesFn = div.msMatchesSelector.bind(div);
+ } else {
+ matchesFn = div.querySelector.bind(div);
+ }
+ return function(s) {
+ try {
+ matchesFn(s + ', ' + s + ':not(#foo)');
+ } catch (ex) {
+ return false;
+ }
+ return true;
+ };
+})();
+
+var reIsRegexLiteral = /^\/.+\/$/;
+
var isBadRegex = function(s) {
try {
void new RegExp(s);
@@ -218,7 +247,7 @@ var FilterParser = function() {
this.hostnames = [];
this.invalid = false;
this.cosmetic = true;
- this.reNeedHostname = /^(?:script:contains|script:inject|.+?:has|.+?:matches-css(?:-before|-after)?|:xpath)\(.+?\)$/;
+ this.reNeedHostname = /^(?:script:contains|script:inject|.+?:has|.+?:has-text|.+?:if|.+?:if-not|.+?:matches-css(?:-before|-after)?|.*?:xpath)\(.+\)$/;
};
/******************************************************************************/
@@ -331,7 +360,12 @@ FilterParser.prototype.parse = function(raw) {
// ##script:contains(...)
// ##script:inject(...)
// ##.foo:has(...)
+ // ##.foo:has-text(...)
+ // ##.foo:if(...)
+ // ##.foo:if-not(...)
// ##.foo:matches-css(...)
+ // ##.foo:matches-css-after(...)
+ // ##.foo:matches-css-before(...)
// ##:xpath(...)
if (
this.hostnames.length === 0 &&
@@ -698,91 +732,178 @@ FilterContainer.prototype.freeze = function() {
// implemented (if ever). Unlikely, see:
// https://github.com/gorhill/uBlock/issues/1752
-FilterContainer.prototype.isValidSelector = (function() {
- var div = document.createElement('div');
- var matchesProp = (function() {
- if ( typeof div.matches === 'function' ) {
- return 'matches';
- }
- if ( typeof div.mozMatchesSelector === 'function' ) {
- return 'mozMatchesSelector';
- }
- if ( typeof div.webkitMatchesSelector === 'function' ) {
- return 'webkitMatchesSelector';
- }
- return '';
- })();
- // Not all browsers support `Element.matches`:
- // http://caniuse.com/#feat=matchesselector
- if ( matchesProp === '' ) {
- return function() {
- return true;
- };
- }
-
- var reHasSelector = /^(.+?):has\((.+?)\)$/,
- reMatchesCSSSelector = /^(.+?):matches-css(?:-before|-after)?\((.+?)\)$/,
- reXpathSelector = /^:xpath\((.+?)\)$/,
- reStyleSelector = /^(.+?):style\((.+?)\)$/,
+FilterContainer.prototype.compileSelector = (function() {
+ var reStyleSelector = /^(.+?):style\((.+?)\)$/,
reStyleBad = /url\([^)]+\)/,
reScriptSelector = /^script:(contains|inject)\((.+)\)$/;
- // Keep in mind:
- // https://github.com/gorhill/uBlock/issues/693
- // https://github.com/gorhill/uBlock/issues/1955
- var isValidCSSSelector = function(s) {
- try {
- div[matchesProp](s + ', ' + s + ':not(#foo)');
- } catch (ex) {
- return false;
+ return function(raw) {
+ if ( isValidCSSSelector(raw) && raw.indexOf('[-abp-properties=') === -1 ) {
+ return raw;
}
- return true;
- };
- return function(s) {
- if ( isValidCSSSelector(s) && s.indexOf('[-abp-properties=') === -1 ) {
- return true;
- }
- // We reach this point very rarely.
+ // We rarely reach this point.
var matches;
- // Future `:has`-based filter? If so, validate both parts of the whole
- // selector.
- matches = reHasSelector.exec(s);
- if ( matches !== null ) {
- return isValidCSSSelector(matches[1]) && isValidCSSSelector(matches[2]);
- }
- // Custom `:matches-css`-based filter?
- matches = reMatchesCSSSelector.exec(s);
- if ( matches !== null ) {
- return isValidCSSSelector(matches[1]);
- }
- // Custom `:xpath`-based filter?
- matches = reXpathSelector.exec(s);
- if ( matches !== null ) {
- try {
- return document.createExpression(matches[1], null) instanceof XPathExpression;
- } catch (e) {
- }
- return false;
- }
// `:style` selector?
- matches = reStyleSelector.exec(s);
- if ( matches !== null ) {
- return isValidCSSSelector(matches[1]) && reStyleBad.test(matches[2]) === false;
- }
- // Special `script:` filter?
- matches = reScriptSelector.exec(s);
- if ( matches !== null ) {
- if ( matches[1] === 'inject' ) {
- return true;
+ if ( (matches = reStyleSelector.exec(raw)) !== null ) {
+ if ( isValidCSSSelector(matches[1]) && reStyleBad.test(matches[2]) === false ) {
+ return JSON.stringify({
+ style: true,
+ raw: raw,
+ parts: [ matches[1], '{' + matches[2] + '}' ]
+ });
}
- return matches[2].startsWith('/') === false ||
- matches[2].endsWith('/') === false ||
- isBadRegex(matches[2].slice(1, -1)) === false;
+ return;
}
- µb.logger.writeOne('', 'error', 'Cosmetic filtering – invalid filter: ' + s);
- return false;
+
+ // `script:` filter?
+ if ( (matches = reScriptSelector.exec(raw)) !== null ) {
+ // :inject
+ if ( matches[1] === 'inject' ) {
+ return raw;
+ }
+ // :contains
+ if ( reIsRegexLiteral.test(matches[2]) === false || isBadRegex(matches[2].slice(1, -1)) === false ) {
+ return raw;
+ }
+ return;
+ }
+
+ // Procedural selector?
+ var compiled;
+ if ( (compiled = this.compileProceduralSelector(raw)) ) {
+ return compiled;
+ }
+
+ µb.logger.writeOne('', 'error', 'Cosmetic filtering – invalid filter: ' + raw);
+ return;
+ };
+})();
+
+/******************************************************************************/
+
+FilterContainer.prototype.compileProceduralSelector = (function() {
+ var reParserEx = /(:(?:has|has-text|if|if-not|matches-css|matches-css-after|matches-css-before|xpath))\(.+\)$/,
+ reFirstParentheses = /^\(*/,
+ reLastParentheses = /\)*$/,
+ reEscapeRegex = /[.*+?^${}()|[\]\\]/g;
+
+ var lastProceduralSelector = '',
+ lastProceduralSelectorCompiled;
+
+ var compileCSSSelector = function(s) {
+ if ( isValidCSSSelector(s) ) {
+ return s;
+ }
+ };
+
+ var compileText = function(s) {
+ if ( reIsRegexLiteral.test(s) ) {
+ s = s.slice(1, -1);
+ if ( isBadRegex(s) ) { return; }
+ } else {
+ s = s.replace(reEscapeRegex, '\\$&');
+ }
+ return s;
+ };
+
+ var compileCSSDeclaration = function(s) {
+ var name, value,
+ pos = s.indexOf(':');
+ if ( pos === -1 ) { return; }
+ name = s.slice(0, pos).trim();
+ value = s.slice(pos + 1).trim();
+ if ( reIsRegexLiteral.test(value) ) {
+ value = value.slice(1, -1);
+ if ( isBadRegex(value) ) { return; }
+ } else {
+ value = value.replace(reEscapeRegex, '\\$&');
+ }
+ return { name: name, value: value };
+ };
+
+ var compileConditionalSelector = function(s) {
+ return compile(s);
+ };
+
+ var compileXpathExpression = function(s) {
+ var dummy;
+ try {
+ dummy = document.createExpression(s, null) instanceof XPathExpression;
+ } catch (e) {
+ return;
+ }
+ return s;
+ };
+
+ var compileArgument = new Map([
+ [ ':has', compileCSSSelector ],
+ [ ':has-text', compileText ],
+ [ ':if', compileConditionalSelector ],
+ [ ':if-not', compileConditionalSelector ],
+ [ ':matches-css', compileCSSDeclaration ],
+ [ ':matches-css-after', compileCSSDeclaration ],
+ [ ':matches-css-before', compileCSSDeclaration ],
+ [ ':xpath', compileXpathExpression ]
+ ]);
+
+ var compile = function(raw) {
+ var matches = reParserEx.exec(raw);
+ if ( matches === null ) { return; }
+ var tasks = [],
+ firstOperand = raw.slice(0, matches.index),
+ currentOperator = matches[1],
+ selector = raw.slice(matches.index + currentOperator.length),
+ currentArgument = '', nextOperand, nextOperator,
+ depth = 0, opening, closing;
+ if ( firstOperand !== '' && isValidCSSSelector(firstOperand) === false ) { return; }
+ for (;;) {
+ matches = reParserEx.exec(selector);
+ if ( matches !== null ) {
+ nextOperand = selector.slice(0, matches.index);
+ nextOperator = matches[1];
+ } else {
+ nextOperand = selector;
+ nextOperator = '';
+ }
+ opening = reFirstParentheses.exec(nextOperand)[0].length;
+ closing = reLastParentheses.exec(nextOperand)[0].length;
+ if ( opening > closing ) {
+ if ( depth === 0 ) { currentArgument = ''; }
+ depth += 1;
+ } else if ( closing > opening && depth > 0 ) {
+ depth -= 1;
+ if ( depth === 0 ) { nextOperand = currentArgument + nextOperand; }
+ }
+ if ( depth !== 0 ) {
+ currentArgument += nextOperand + nextOperator;
+ } else {
+ currentArgument = compileArgument.get(currentOperator)(nextOperand.slice(1, -1));
+ if ( currentArgument === undefined ) { return; }
+ tasks.push([ currentOperator, currentArgument ]);
+ currentOperator = nextOperator;
+ }
+ if ( nextOperator === '' ) { break; }
+ selector = selector.slice(matches.index + nextOperator.length);
+ }
+ if ( tasks.length === 0 || depth !== 0 ) { return; }
+ return { selector: firstOperand, tasks: tasks };
+ };
+
+ return function(raw) {
+ if ( raw === lastProceduralSelector ) {
+ return lastProceduralSelectorCompiled;
+ }
+ lastProceduralSelector = raw;
+ var compiled = compile(raw);
+ if ( compiled !== undefined ) {
+ compiled.procedural = true;
+ compiled.raw = raw;
+ compiled = JSON.stringify(compiled);
+ }
+ lastProceduralSelectorCompiled = compiled;
+ return compiled;
};
})();
@@ -843,7 +964,7 @@ FilterContainer.prototype.compile = function(s, out) {
// still the most common, and can easily be tested using a plain regex.
if (
this.reClassOrIdSelector.test(parsed.suffix) === false &&
- this.isValidSelector(parsed.suffix) === false
+ this.compileSelector(parsed.suffix) === undefined
) {
return true;
}
@@ -895,15 +1016,15 @@ FilterContainer.prototype.compileGenericHideSelector = function(parsed, out) {
return;
}
// Composite CSS rule.
- if ( this.isValidSelector(selector) ) {
+ if ( this.compileSelector(selector) ) {
out.push('c\vlg+\v' + key + '\v' + selector);
}
return;
}
- if ( this.isValidSelector(selector) !== true ) {
- return;
- }
+ var compiled = this.compileSelector(selector);
+ if ( compiled === undefined ) { return; }
+ // TODO: Detect and error on procedural cosmetic filters.
// ["title"] and ["alt"] will go in high-low generic bin.
if ( this.reHighLow.test(selector) ) {
@@ -948,10 +1069,6 @@ FilterContainer.prototype.compileGenericHideSelector = function(parsed, out) {
FilterContainer.prototype.compileGenericUnhideSelector = function(parsed, out) {
var selector = parsed.suffix;
- if ( this.isValidSelector(selector) !== true ) {
- return;
- }
-
// script:contains(...)
// script:inject(...)
if ( this.reScriptSelector.test(selector) ) {
@@ -959,10 +1076,14 @@ FilterContainer.prototype.compileGenericUnhideSelector = function(parsed, out) {
return;
}
+ // Procedural cosmetic filters are acceptable as generic exception filters.
+ var compiled = this.compileSelector(selector);
+ if ( compiled === undefined ) { return; }
+
// https://github.com/chrisaljoudi/uBlock/issues/497
// All generic exception filters are put in the same bucket: they are
// expected to be very rare.
- out.push('c\vg1\v' + selector);
+ out.push('c\vg1\v' + compiled);
};
/******************************************************************************/
@@ -980,20 +1101,24 @@ FilterContainer.prototype.compileHostnameSelector = function(hostname, parsed, o
hostname = this.punycode.toASCII(hostname);
}
- var domain = this.µburi.domainFromHostname(hostname),
+ var selector = parsed.suffix,
+ domain = this.µburi.domainFromHostname(hostname),
hash;
// script:contains(...)
// script:inject(...)
- if ( this.reScriptSelector.test(parsed.suffix) ) {
+ if ( this.reScriptSelector.test(selector) ) {
hash = domain !== '' ? domain : this.noDomainHash;
if ( unhide ) {
hash = '!' + hash;
}
- out.push('c\vjs\v' + hash + '\v' + hostname + '\v' + parsed.suffix);
+ out.push('c\vjs\v' + hash + '\v' + hostname + '\v' + selector);
return;
}
+ var compiled = this.compileSelector(selector);
+ if ( compiled === undefined ) { return; }
+
// https://github.com/chrisaljoudi/uBlock/issues/188
// If not a real domain as per PSL, assign a synthetic one
if ( hostname.endsWith('.*') === false ) {
@@ -1005,7 +1130,7 @@ FilterContainer.prototype.compileHostnameSelector = function(hostname, parsed, o
hash = '!' + hash;
}
- out.push('c\vh\v' + hash + '\v' + hostname + '\v' + parsed.suffix);
+ out.push('c\vh\v' + hash + '\v' + hostname + '\v' + compiled);
};
/******************************************************************************/
diff --git a/src/js/messaging.js b/src/js/messaging.js
index b422d1abc..c6ade1f4f 100644
--- a/src/js/messaging.js
+++ b/src/js/messaging.js
@@ -100,6 +100,10 @@ var onMessage = function(request, sender, callback) {
µb.mouseURL = request.url;
break;
+ case 'compileCosmeticFilterSelector':
+ response = µb.cosmeticFilteringEngine.compileSelector(request.selector);
+ break;
+
case 'cosmeticFiltersInjected':
µb.cosmeticFilteringEngine.addToSelectorCache(request);
/* falls through */
diff --git a/src/js/reverselookup-worker.js b/src/js/reverselookup-worker.js
index 176a2896c..ff827fdcd 100644
--- a/src/js/reverselookup-worker.js
+++ b/src/js/reverselookup-worker.js
@@ -134,14 +134,23 @@ var fromCosmeticFilter = function(details) {
}
candidates[details.rawFilter] = new RegExp(reStr.join('\\v') + '(?:\\n|$)');
+ // Procedural filters, which are pre-compiled, make thing sort of
+ // complicated. We are going to also search for one portion of the
+ // compiled form of a filter.
+ var filterEx = '(' +
+ reEscape(filter) +
+ '|[^\\v]+' +
+ reEscape(JSON.stringify({ raw: filter }).slice(1,-1)) +
+ '[^\\v]+)';
+
// Second step: find hostname-based versions.
// Reference: FilterContainer.compileHostnameSelector().
- var pos;
- var hostname = details.hostname;
+ var pos,
+ hostname = details.hostname;
if ( hostname !== '' ) {
for ( ;; ) {
candidates[hostname + '##' + filter] = new RegExp(
- ['c', 'h', '[^\\v]+', reEscape(hostname), reEscape(filter)].join('\\v') +
+ ['c', 'h', '[^\\v]+', reEscape(hostname), filterEx].join('\\v') +
'(?:\\n|$)'
);
pos = hostname.indexOf('.');
@@ -159,7 +168,7 @@ var fromCosmeticFilter = function(details) {
if ( pos !== -1 ) {
var entity = domain.slice(0, pos) + '.*';
candidates[entity + '##' + filter] = new RegExp(
- ['c', 'h', '[^\\v]+', reEscape(entity), reEscape(filter)].join('\\v') +
+ ['c', 'h', '[^\\v]+', reEscape(entity), filterEx].join('\\v') +
'(?:\\n|$)'
);
}
diff --git a/src/js/scriptlets/cosmetic-logger.js b/src/js/scriptlets/cosmetic-logger.js
index 29ac23477..51c67d6fe 100644
--- a/src/js/scriptlets/cosmetic-logger.js
+++ b/src/js/scriptlets/cosmetic-logger.js
@@ -31,38 +31,36 @@ if ( typeof vAPI !== 'object' || !vAPI.domFilterer ) {
return;
}
-var df = vAPI.domFilterer,
- loggedSelectors = vAPI.loggedSelectors || {},
- matchedSelectors = [],
- selectors, i, selector;
+var loggedSelectors = vAPI.loggedSelectors || {},
+ matchedSelectors = [];
-// CSS selectors.
-selectors = df.jobQueue[2]._0.concat(df.jobQueue[3]._0);
-i = selectors.length;
-while ( i-- ) {
- selector = selectors[i];
- if ( loggedSelectors.hasOwnProperty(selector) ) {
- continue;
+var evaluateSelector = function(selector) {
+ if (
+ loggedSelectors.hasOwnProperty(selector) === false &&
+ document.querySelector(selector) !== null
+ ) {
+ loggedSelectors[selector] = true;
+ matchedSelectors.push(selector);
}
- if ( document.querySelector(selector) === null ) {
- continue;
- }
- loggedSelectors[selector] = true;
- matchedSelectors.push(selector);
-}
-
-// Non-CSS selectors.
-var logHit = function(node, job) {
- if ( !job.raw || loggedSelectors.hasOwnProperty(job.raw) ) {
- return;
- }
- loggedSelectors[job.raw] = true;
- matchedSelectors.push(job.raw);
};
-for ( i = 4; i < df.jobQueue.length; i++ ) {
- df.runJob(df.jobQueue[i], logHit);
-}
+
+// Simple CSS selector-based cosmetic filters.
+vAPI.domFilterer.simpleHideSelectors.entries.forEach(evaluateSelector);
+
+// Complex CSS selector-based cosmetic filters.
+vAPI.domFilterer.complexHideSelectors.entries.forEach(evaluateSelector);
+
+// Procedural cosmetic filters.
+vAPI.domFilterer.proceduralSelectors.entries.forEach(function(pfilter) {
+ if (
+ loggedSelectors.hasOwnProperty(pfilter.raw) === false &&
+ pfilter.exec().length !== 0
+ ) {
+ loggedSelectors[pfilter.raw] = true;
+ matchedSelectors.push(pfilter.raw);
+ }
+});
vAPI.loggedSelectors = loggedSelectors;
diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js
index a31f6988a..5ef7658b6 100644
--- a/src/js/scriptlets/dom-inspector.js
+++ b/src/js/scriptlets/dom-inspector.js
@@ -693,7 +693,7 @@ var cosmeticFilterMapper = (function() {
i, j;
// CSS-based selectors: simple one.
- selectors = vAPI.domFilterer.job2._0;
+ selectors = vAPI.domFilterer.simpleHideSelectors.entries;
i = selectors.length;
while ( i-- ) {
selector = selectors[i];
@@ -709,9 +709,9 @@ var cosmeticFilterMapper = (function() {
}
}
}
-
+
// CSS-based selectors: complex one (must query from doc root).
- selectors = vAPI.domFilterer.job3._0;
+ selectors = vAPI.domFilterer.complexHideSelectors.entries;
i = selectors.length;
while ( i-- ) {
selector = selectors[i];
@@ -726,14 +726,12 @@ var cosmeticFilterMapper = (function() {
}
// Non-CSS selectors.
- var runJobCallback = function(node, job) {
+ var runJobCallback = function(node, pfilter) {
if ( filterMap.has(node) === false ) {
- filterMap.set(node, job.raw);
+ filterMap.set(node, pfilter.raw);
}
};
- for ( i = 4; i < vAPI.domFilterer.jobQueue.length; i++ ) {
- vAPI.domFilterer.runJob(vAPI.domFilterer.jobQueue[i], runJobCallback);
- }
+ vAPI.domFilterer.proceduralSelectors.forEachNode(runJobCallback);
};
var incremental = function(rootNode) {
diff --git a/src/js/scriptlets/element-picker.js b/src/js/scriptlets/element-picker.js
index fc77cbdd0..1181f77f5 100644
--- a/src/js/scriptlets/element-picker.js
+++ b/src/js/scriptlets/element-picker.js
@@ -177,6 +177,14 @@ var safeQuerySelectorAll = function(node, selector) {
/******************************************************************************/
+var rawFilterFromTextarea = function() {
+ var s = taCandidate.value,
+ pos = s.indexOf('\n');
+ return pos === -1 ? s.trim() : s.slice(0, pos).trim();
+};
+
+/******************************************************************************/
+
var getElementBoundingClientRect = function(elem) {
var rect = typeof elem.getBoundingClientRect === 'function' ?
elem.getBoundingClientRect() :
@@ -635,7 +643,9 @@ var filtersFrom = function(x, y) {
filterToDOMInterface.set
@desc Look-up all the HTML elements matching the filter passed in
argument.
- @param string, a cosmetic of network filter.
+ @param string, a cosmetic or network filter.
+ @param function, called once all items matching the filter have been
+ collected.
@return array, or undefined if the filter is invalid.
filterToDOMInterface.preview
@@ -733,16 +743,15 @@ var filterToDOMInterface = (function() {
// ways to compose a valid href to the same effective URL. One idea is to
// normalize all a[href] on the page, but for now I will wait and see, as I
// prefer to refrain from tampering with the page content if I can avoid it.
- var fromCosmeticFilter = function(filter) {
+ var fromPlainCosmeticFilter = function(filter) {
var elems;
try {
elems = document.querySelectorAll(filter);
}
catch (e) {
- return fromProceduralCosmeticFilter(filter);
+ return;
}
- var out = [],
- iElem = elems.length;
+ var out = [], iElem = elems.length;
while ( iElem-- ) {
out.push({ type: 'cosmetic', elem: elems[iElem]});
}
@@ -751,108 +760,27 @@ var filterToDOMInterface = (function() {
// https://github.com/gorhill/uBlock/issues/1772
// Handle procedural cosmetic filters.
- var fromProceduralCosmeticFilter = function(filter) {
- if ( filter.charCodeAt(filter.length - 1) === 0x29 /* ')' */ ) {
- var parts = reProceduralCosmeticFilter.exec(filter);
- if (
- parts !== null &&
- proceduralCosmeticFilterFunctions.hasOwnProperty(parts[2])
- ) {
- return proceduralCosmeticFilterFunctions[parts[2]](
- parts[1].trim(),
- parts[3].trim()
- );
- }
+ var fromCompiledCosmeticFilter = function(raw) {
+ if ( typeof raw !== 'string' ) { return; }
+ var o;
+ try {
+ o = JSON.parse(raw);
+ } catch(ex) {
+ return;
}
- };
-
- var reProceduralCosmeticFilter = /^(.*?):(matches-css|has|style|xpath)\((.+?)\)$/;
-
- // Collection of handlers for procedural cosmetic filters.
- var proceduralCosmeticFilterFunctions = {
- 'has': function(selector, arg) {
- if ( selector === '' ) { return; }
- var elems;
- try {
- elems = document.querySelectorAll(selector);
- document.querySelector(arg);
- } catch(ex) {
- return;
- }
- var out = [], elem;
- for ( var i = 0, n = elems.length; i < n; i++ ) {
- elem = elems[i];
- if ( elem.querySelector(arg) ) {
- out.push({ type: 'cosmetic', elem: elem });
- }
- }
- return out;
- },
- 'matches-css': function(selector, arg) {
- if ( selector === '' ) { return; }
- var elems;
- try {
- elems = document.querySelectorAll(selector);
- } catch(ex) {
- return;
- }
- var out = [], elem, style,
- pos = arg.indexOf(':');
- if ( pos === -1 ) { return; }
- var prop = arg.slice(0, pos).trim(),
- reText = arg.slice(pos + 1).trim();
- if ( reText === '' ) { return; }
- var re = reText !== '*' ?
- new RegExp('^' + reText.replace(/[.+?${}()|[\]\\^]/g, '\\$&').replace(/\*+/g, '.*?') + '$') :
- /./;
- for ( var i = 0, n = elems.length; i < n; i++ ) {
- elem = elems[i];
- style = window.getComputedStyle(elem, null);
- if ( re.test(style[prop]) ) {
- out.push({ type: 'cosmetic', elem: elem });
- }
- }
- return out;
- },
- 'style': function(selector, arg) {
- if ( selector === '' || arg === '' ) { return; }
- var elems;
- try {
- elems = document.querySelectorAll(selector);
- } catch(ex) {
- return;
- }
- var out = [];
- for ( var i = 0, n = elems.length; i < n; i++ ) {
- out.push({ type: 'cosmetic', elem: elems[i] });
- }
- lastAction = selector + ' { ' + arg + ' }';
- return out;
- },
- 'xpath': function(selector, arg) {
- if ( selector !== '' ) { return []; }
- var result;
- try {
- result = document.evaluate(
- arg,
- document,
- null,
- XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
- null
- );
- } catch(ex) {
- return;
- }
- if ( result === undefined ) { return []; }
- var out = [], elem, i = result.snapshotLength;
- while ( i-- ) {
- elem = result.snapshotItem(i);
- if ( elem.nodeType === 1 ) {
- out.push({ type: 'cosmetic', elem: elem });
- }
- }
- return out;
+ var elems;
+ if ( o.style ) {
+ elems = document.querySelectorAll(o.parts[0]);
+ lastAction = o.parts.join(' ');
+ } else if ( o.procedural ) {
+ elems = vAPI.domFilterer.createProceduralFilter(o).exec();
}
+ if ( !elems ) { return; }
+ var out = [];
+ for ( var i = 0, n = elems.length; i < n; i++ ) {
+ out.push({ type: 'cosmetic', elem: elems[i] });
+ }
+ return out;
};
var lastFilter,
@@ -862,26 +790,44 @@ var filterToDOMInterface = (function() {
applied = false,
previewing = false;
- var queryAll = function(filter) {
+ var queryAll = function(filter, callback) {
filter = filter.trim();
if ( filter === lastFilter ) {
- return lastResultset;
+ callback(lastResultset);
+ return;
}
unapply();
if ( filter === '' ) {
lastFilter = '';
lastResultset = [];
- } else {
- lastFilter = filter;
- lastAction = undefined;
- lastResultset = filter.lastIndexOf('##', 0) === 0 ?
- fromCosmeticFilter(filter.slice(2)) :
- fromNetworkFilter(filter);
- if ( previewing ) {
- apply(filter);
- }
+ callback(lastResultset);
+ return;
}
- return lastResultset;
+ lastFilter = filter;
+ lastAction = undefined;
+ if ( filter.lastIndexOf('##', 0) === -1 ) {
+ lastResultset = fromNetworkFilter(filter);
+ if ( previewing ) { apply(); }
+ callback(lastResultset);
+ return;
+ }
+ var selector = filter.slice(2);
+ lastResultset = fromPlainCosmeticFilter(selector);
+ if ( lastResultset ) {
+ if ( previewing ) { apply(); }
+ callback(lastResultset);
+ return;
+ }
+ // Procedural cosmetic filter
+ vAPI.messaging.send(
+ 'elementPicker',
+ { what: 'compileCosmeticFilterSelector', selector: selector },
+ function(response) {
+ lastResultset = fromCompiledCosmeticFilter(response);
+ if ( previewing ) { apply(); }
+ callback(lastResultset);
+ }
+ );
};
var applyHide = function() {
@@ -983,9 +929,11 @@ var filterToDOMInterface = (function() {
var preview = function(filter) {
previewing = filter !== false;
if ( previewing ) {
- if ( queryAll(filter) !== undefined ) {
- apply();
- }
+ queryAll(filter, function(items) {
+ if ( items !== undefined ) {
+ apply();
+ }
+ });
} else {
unapply();
}
@@ -999,70 +947,75 @@ var filterToDOMInterface = (function() {
};
})();
-// https://www.youtube.com/watch?v=nuUXJ6RfIik
-
/******************************************************************************/
-var userFilterFromCandidate = function() {
- var v = taCandidate.value;
- var items = filterToDOMInterface.set(v);
- if ( !items || items.length === 0 ) {
- return false;
- }
-
- // https://github.com/gorhill/uBlock/issues/738
- // Trim dots.
- var hostname = window.location.hostname;
- if ( hostname.slice(-1) === '.' ) {
- hostname = hostname.slice(0, -1);
- }
-
- // Cosmetic filter?
- if ( v.lastIndexOf('##', 0) === 0 ) {
- return hostname + v;
- }
-
- // Assume net filter
- var opts = [];
-
- // If no domain included in filter, we need domain option
- if ( v.lastIndexOf('||', 0) === -1 ) {
- opts.push('domain=' + hostname);
- }
-
- var item = items[0];
- if ( item.opts ) {
- opts.push(item.opts);
- }
-
- if ( opts.length ) {
- v += '$' + opts.join(',');
- }
-
- return v;
-};
-
-/******************************************************************************/
-
-var onCandidateChanged = function() {
- var elems = [],
- items = filterToDOMInterface.set(taCandidate.value),
- valid = items !== undefined;
- if ( valid ) {
- for ( var i = 0; i < items.length; i++ ) {
- elems.push(items[i].elem);
+var userFilterFromCandidate = function(callback) {
+ var v = rawFilterFromTextarea();
+ filterToDOMInterface.set(v, function(items) {
+ if ( !items || items.length === 0 ) {
+ callback();
+ return;
}
- }
- pickerBody.querySelector('body section textarea + div').textContent = valid ?
- items.length.toLocaleString() :
- '0';
- taCandidate.classList.toggle('invalidFilter', !valid);
- dialog.querySelector('#create').disabled = elems.length === 0;
- highlightElements(elems, true);
+
+ // https://github.com/gorhill/uBlock/issues/738
+ // Trim dots.
+ var hostname = window.location.hostname;
+ if ( hostname.slice(-1) === '.' ) {
+ hostname = hostname.slice(0, -1);
+ }
+
+ // Cosmetic filter?
+ if ( v.lastIndexOf('##', 0) === 0 ) {
+ callback(hostname + v);
+ return;
+ }
+
+ // Assume net filter
+ var opts = [];
+
+ // If no domain included in filter, we need domain option
+ if ( v.lastIndexOf('||', 0) === -1 ) {
+ opts.push('domain=' + hostname);
+ }
+
+ var item = items[0];
+ if ( item.opts ) {
+ opts.push(item.opts);
+ }
+
+ if ( opts.length ) {
+ v += '$' + opts.join(',');
+ }
+
+ callback(v);
+ });
};
/******************************************************************************/
+var onCandidateChanged = (function() {
+ var process = function(items) {
+ var elems = [], valid = items !== undefined;
+ if ( valid ) {
+ for ( var i = 0; i < items.length; i++ ) {
+ elems.push(items[i].elem);
+ }
+ }
+ pickerBody.querySelector('body section textarea + div').textContent = valid ?
+ items.length.toLocaleString() :
+ 'ε';
+ dialog.querySelector('section').classList.toggle('invalidFilter', !valid);
+ dialog.querySelector('#create').disabled = elems.length === 0;
+ highlightElements(elems, true);
+ };
+
+ return function() {
+ filterToDOMInterface.set(rawFilterFromTextarea(), process);
+ };
+})();
+
+/******************************************************************************/
+
var candidateFromFilterChoice = function(filterChoice) {
var slot = filterChoice.slot;
var filters = filterChoice.filters;
@@ -1132,8 +1085,8 @@ var onDialogClicked = function(ev) {
// We have to exit from preview mode: this guarantees matching elements
// will be found for the candidate filter.
filterToDOMInterface.preview(false);
- var filter = userFilterFromCandidate();
- if ( filter ) {
+ userFilterFromCandidate(function(filter) {
+ if ( !filter ) { return; }
var d = new Date();
vAPI.messaging.send(
'elementPicker',
@@ -1143,9 +1096,9 @@ var onDialogClicked = function(ev) {
pageDomain: window.location.hostname
}
);
- filterToDOMInterface.preview(taCandidate.value);
+ filterToDOMInterface.preview(rawFilterFromTextarea());
stopPicker();
- }
+ });
}
else if ( ev.target.id === 'pick' ) {
@@ -1161,7 +1114,7 @@ var onDialogClicked = function(ev) {
if ( filterToDOMInterface.previewing() ) {
filterToDOMInterface.preview(false);
} else {
- filterToDOMInterface.preview(taCandidate.value);
+ filterToDOMInterface.preview(rawFilterFromTextarea());
}
highlightElements(targetElements, true);
}
@@ -1300,6 +1253,7 @@ var onKeyPressed = function(ev) {
if ( ev.which === 27 ) {
ev.stopPropagation();
ev.preventDefault();
+ filterToDOMInterface.preview(false);
stopPicker();
}
};