1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-09-18 08:52:26 +02:00

add chainable and recursive cosmetic procedural filters

This commit is contained in:
gorhill 2016-12-25 16:56:39 -05:00
parent 2f01fcda54
commit 73a69711f2
8 changed files with 654 additions and 497 deletions

View File

@ -63,7 +63,10 @@ section > div:first-child {
margin: 0; margin: 0;
position: relative; position: relative;
} }
section > div > textarea { section.invalidFilter > div:first-child {
border-color: red;
}
section > div:first-child > textarea {
background-color: #fff; background-color: #fff;
border: none; border: none;
box-sizing: border-box; box-sizing: border-box;
@ -75,10 +78,7 @@ section > div > textarea {
resize: none; resize: none;
width: 100%; width: 100%;
} }
section > div > textarea.invalidFilter { section > div:first-child > textarea + div {
background-color: #fee;
}
section > div > textarea + div {
background-color: #aaa; background-color: #aaa;
bottom: 0; bottom: 0;
color: white; color: white;
@ -86,6 +86,9 @@ section > div > textarea + div {
position: absolute; position: absolute;
right: 0; right: 0;
} }
section.invalidFilter > div:first-child > textarea + div {
background-color: red;
}
section > div:first-child + div { section > div:first-child + div {
direction: ltr; direction: ltr;
margin: 2px 0; margin: 2px 0;

View File

@ -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(), var allExceptions = createSet(),
allSelectors = createSet(), allSelectors = createSet(),
stagedNodes = [], stagedNodes = [];
matchesProp = vAPI.matchesProp;
// Complex selectors, due to their nature may need to be "de-committed". A // Complex selectors, due to their nature may need to be "de-committed". A
// Set() is used to implement this functionality. // Set() is used to implement this functionality.
@ -308,100 +298,179 @@ var platformHideNode = vAPI.hideNode,
/******************************************************************************/ /******************************************************************************/
var runSimpleSelectorJob = function(job, root, fn) { // 'P' stands for 'Procedural'
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);
}
};
var runComplexSelectorJob = function(job, fn) { var PSelectorHasTask = function(task) {
if ( job._1 === undefined ) { this.selector = task[1];
job._1 = job._0.join(',');
}
var nodes = document.querySelectorAll(job._1),
i = nodes.length;
while ( i-- ) {
fn(nodes[i], job);
}
}; };
PSelectorHasTask.prototype.exec = function(input) {
var runHasJob = function(job, fn) { var output = [];
var nodes = document.querySelectorAll(job._0), for ( var i = 0, n = input.length; i < n; i++ ) {
i = nodes.length, node; if ( input[i].querySelector(this.selector) !== null ) {
while ( i-- ) { output.push(input[i]);
node = nodes[i];
if ( node.querySelector(job._1) !== null ) {
fn(node, job);
} }
} }
return output;
}; };
// '/' = ascii 0x2F */ var PSelectorHasTextTask = function(task) {
this.needle = new RegExp(task[1]);
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 };
}; };
PSelectorHasTextTask.prototype.exec = function(input) {
var runMatchesCSSJob = function(job, fn) { var output = [];
var nodes = document.querySelectorAll(job._0), for ( var i = 0, n = input.length; i < n; i++ ) {
i = nodes.length; if ( this.needle.test(input[i].textContent) ) {
if ( i === 0 ) { return; } output.push(input[i]);
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);
} }
} }
return output;
}; };
var runXpathJob = function(job, fn) { var PSelectorIfTask = function(task) {
if ( job._1 === undefined ) { this.pselector = new PSelector(task[1]);
job._1 = document.createExpression(job._0, null); };
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( return output;
document, };
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
job._2 || null var PSelectorIfNotTask = function(task) {
); PSelectorIfTask.call(this, task);
var i = xpr.snapshotLength, node; 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-- ) { while ( i-- ) {
node = xpr.snapshotItem(i); pfilter = pfilters[i];
if ( node.nodeType === 1 ) { nodes = pfilter.exec();
fn(node, job); j = nodes.length;
while ( j-- ) {
callback(nodes[j], pfilter);
} }
} }
}; };
@ -418,14 +487,52 @@ var domFilterer = {
hiddenNodeCount: 0, hiddenNodeCount: 0,
hiddenNodeEnforcer: false, hiddenNodeEnforcer: false,
loggerEnabled: undefined, loggerEnabled: undefined,
styleTags: [],
jobQueue: jobQueue, newHideSelectorBuffer: [], // Hide style filter buffer
// Stock jobs. newStyleRuleBuffer: [], // Non-hide style filter buffer
job0: jobQueue[0], simpleHideSelectors: { // Hiding filters: simple selectors
job1: jobQueue[1], entries: [],
job2: jobQueue[2], matchesProp: vAPI.matchesProp,
job3: jobQueue[3], 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) { addExceptions: function(aa) {
for ( var i = 0, n = aa.length; i < n; i++ ) { for ( var i = 0, n = aa.length; i < n; i++ ) {
@ -433,58 +540,28 @@ var domFilterer = {
} }
}, },
// Job: addSelector: function(selector) {
// Stock jobs in job queue: if ( allSelectors.has(selector) || allExceptions.has(selector) ) {
// 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) ) {
return; return;
} }
allSelectors.add(s); allSelectors.add(selector);
var sel0 = s, sel1 = ''; if ( selector.charCodeAt(0) !== 0x7B /* '{' */ ) {
if ( s.charCodeAt(s.length - 1) === 0x29 ) { this.newHideSelectorBuffer.push(selector);
var parts = reParserEx.exec(s); if ( selector.indexOf(' ') === -1 ) {
if ( parts !== null ) { this.simpleHideSelectors.add(selector);
sel1 = parts[0];
}
}
if ( sel1 === '' ) {
this.job0._0.push(sel0);
if ( sel0.indexOf(' ') === -1 ) {
this.job2._0.push(sel0);
this.job2._1 = undefined;
} else { } else {
this.job3._0.push(sel0); this.complexHideSelectors.add(selector);
this.job3._1 = undefined;
} }
return; return;
} }
sel0 = sel0.slice(0, sel0.length - sel1.length); var o = JSON.parse(selector);
if ( sel1.lastIndexOf(':has', 0) === 0 ) { if ( o.style ) {
this.jobQueue.push({ t: 'has-hide', raw: s, _0: sel0, _1: sel1.slice(5, -1) }); this.newStyleRuleBuffer.push(o.parts.join(' '));
} else if ( sel1.lastIndexOf(':matches-css', 0) === 0 ) { return;
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' }); if ( o.procedural ) {
} else if ( sel1.lastIndexOf(':matches-css-after', 0) === 0 ) { this.proceduralSelectors.add(o);
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) });
} }
return;
}, },
addSelectors: function(aa) { addSelectors: function(aa) {
@ -497,27 +574,27 @@ var domFilterer = {
this.commitTimer.clear(); this.commitTimer.clear();
var beforeHiddenNodeCount = this.hiddenNodeCount, var beforeHiddenNodeCount = this.hiddenNodeCount,
styleText = '', i, n; styleText = '', i;
// Stock job 0 = css rules/hide // CSS rules/hide
if ( this.job0._0.length ) { if ( this.newHideSelectorBuffer.length ) {
styleText = '\n:root ' + this.job0._0.join(',\n:root ') + '\n{ display: none !important; }'; styleText = '\n:root ' + this.newHideSelectorBuffer.join(',\n:root ') + '\n{ display: none !important; }';
this.job0._0.length = 0; this.newHideSelectorBuffer.length = 0;
} }
// Stock job 1 = css rules/any css declaration // CSS rules/any css declaration
if ( this.job1._0.length ) { if ( this.newStyleRuleBuffer.length ) {
styleText += '\n' + this.job1._0.join('\n'); styleText += '\n' + this.newStyleRuleBuffer.join('\n');
this.job1._0.length = 0; this.newStyleRuleBuffer.length = 0;
} }
// Simple selectors: incremental. // Simple selectors: incremental.
// Stock job 2 = simple css selectors/hide // Simple css selectors/hide
if ( this.job2._0.length ) { if ( this.simpleHideSelectors.entries.length ) {
i = stagedNodes.length; i = stagedNodes.length;
while ( i-- ) { while ( i-- ) {
runSimpleSelectorJob(this.job2, stagedNodes[i], hideNode); this.simpleHideSelectors.forEachNode(hideNode, stagedNodes[i], cssNotHiddenId);
} }
} }
stagedNodes = []; stagedNodes = [];
@ -526,17 +603,16 @@ var domFilterer = {
complexSelectorsOldResultSet = complexSelectorsCurrentResultSet; complexSelectorsOldResultSet = complexSelectorsCurrentResultSet;
complexSelectorsCurrentResultSet = createSet('object'); 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 // The handling of these can be considered optional, since they are
// also applied declaratively using a style tag. // also applied declaratively using a style tag.
if ( this.job3._0.length ) { if ( this.complexHideSelectors.entries.length ) {
runComplexSelectorJob(this.job3, complexHideNode); this.complexHideSelectors.forEachNode(complexHideNode);
} }
// Custom jobs. No optional since they can't be applied in a // Procedural cosmetic filters
// declarative way. if ( this.proceduralSelectors.entries.length ) {
for ( i = 4, n = this.jobQueue.length; i < n; i++ ) { this.proceduralSelectors.forEachNode(complexHideNode);
this.runJob(this.jobQueue[i], complexHideNode);
} }
// https://github.com/gorhill/uBlock/issues/1912 // https://github.com/gorhill/uBlock/issues/1912
@ -595,6 +671,10 @@ var domFilterer = {
this.commitTimer.start(); this.commitTimer.start();
}, },
createProceduralFilter: function(o) {
return new PSelector(o);
},
getExcludeId: function() { getExcludeId: function() {
if ( this.excludeId === undefined ) { if ( this.excludeId === undefined ) {
this.excludeId = vAPI.randomToken(); this.excludeId = vAPI.randomToken();
@ -616,20 +696,6 @@ var domFilterer = {
this.commitTimer = new vAPI.SafeAnimationFrame(this.commit_.bind(this)); 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) { showNode: function(node) {
node.hidden = false; node.hidden = false;
platformUnhideNode(node); platformUnhideNode(node);
@ -1248,14 +1314,14 @@ vAPI.domSurveyor = (function() {
// Need to do this before committing DOM filterer, as needed info // Need to do this before committing DOM filterer, as needed info
// will no longer be there after commit. // will no longer be there after commit.
if ( firstSurvey || domFilterer.job0._0.length ) { if ( firstSurvey || domFilterer.newHideSelectorBuffer.length ) {
messaging.send( messaging.send(
'contentscript', 'contentscript',
{ {
what: 'cosmeticFiltersInjected', what: 'cosmeticFiltersInjected',
type: 'cosmetic', type: 'cosmetic',
hostname: window.location.hostname, hostname: window.location.hostname,
selectors: domFilterer.job0._0, selectors: domFilterer.newHideSelectorBuffer,
first: firstSurvey, first: firstSurvey,
cost: surveyCost cost: surveyCost
} }
@ -1263,7 +1329,7 @@ vAPI.domSurveyor = (function() {
} }
// Shutdown surveyor if too many consecutive empty resultsets. // Shutdown surveyor if too many consecutive empty resultsets.
if ( domFilterer.job0._0.length === 0 ) { if ( domFilterer.newHideSelectorBuffer.length === 0 ) {
cosmeticSurveyingMissCount += 1; cosmeticSurveyingMissCount += 1;
} else { } else {
cosmeticSurveyingMissCount = 0; cosmeticSurveyingMissCount = 0;

View File

@ -39,6 +39,35 @@ var µb = µBlock;
var encode = JSON.stringify; var encode = JSON.stringify;
var decode = JSON.parse; 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) { var isBadRegex = function(s) {
try { try {
void new RegExp(s); void new RegExp(s);
@ -218,7 +247,7 @@ var FilterParser = function() {
this.hostnames = []; this.hostnames = [];
this.invalid = false; this.invalid = false;
this.cosmetic = true; 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:contains(...)
// ##script:inject(...) // ##script:inject(...)
// ##.foo:has(...) // ##.foo:has(...)
// ##.foo:has-text(...)
// ##.foo:if(...)
// ##.foo:if-not(...)
// ##.foo:matches-css(...) // ##.foo:matches-css(...)
// ##.foo:matches-css-after(...)
// ##.foo:matches-css-before(...)
// ##:xpath(...) // ##:xpath(...)
if ( if (
this.hostnames.length === 0 && this.hostnames.length === 0 &&
@ -698,91 +732,178 @@ FilterContainer.prototype.freeze = function() {
// implemented (if ever). Unlikely, see: // implemented (if ever). Unlikely, see:
// https://github.com/gorhill/uBlock/issues/1752 // https://github.com/gorhill/uBlock/issues/1752
FilterContainer.prototype.isValidSelector = (function() { FilterContainer.prototype.compileSelector = (function() {
var div = document.createElement('div'); var reStyleSelector = /^(.+?):style\((.+?)\)$/,
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\((.+?)\)$/,
reStyleBad = /url\([^)]+\)/, reStyleBad = /url\([^)]+\)/,
reScriptSelector = /^script:(contains|inject)\((.+)\)$/; reScriptSelector = /^script:(contains|inject)\((.+)\)$/;
// Keep in mind: return function(raw) {
// https://github.com/gorhill/uBlock/issues/693 if ( isValidCSSSelector(raw) && raw.indexOf('[-abp-properties=') === -1 ) {
// https://github.com/gorhill/uBlock/issues/1955 return raw;
var isValidCSSSelector = function(s) {
try {
div[matchesProp](s + ', ' + s + ':not(#foo)');
} catch (ex) {
return false;
} }
return true;
};
return function(s) { // We rarely reach this point.
if ( isValidCSSSelector(s) && s.indexOf('[-abp-properties=') === -1 ) {
return true;
}
// We reach this point very rarely.
var matches; 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? // `:style` selector?
matches = reStyleSelector.exec(s); if ( (matches = reStyleSelector.exec(raw)) !== null ) {
if ( matches !== null ) { if ( isValidCSSSelector(matches[1]) && reStyleBad.test(matches[2]) === false ) {
return isValidCSSSelector(matches[1]) && reStyleBad.test(matches[2]) === false; return JSON.stringify({
} style: true,
// Special `script:` filter? raw: raw,
matches = reScriptSelector.exec(s); parts: [ matches[1], '{' + matches[2] + '}' ]
if ( matches !== null ) { });
if ( matches[1] === 'inject' ) {
return true;
} }
return matches[2].startsWith('/') === false || return;
matches[2].endsWith('/') === false ||
isBadRegex(matches[2].slice(1, -1)) === false;
} }
µ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. // still the most common, and can easily be tested using a plain regex.
if ( if (
this.reClassOrIdSelector.test(parsed.suffix) === false && this.reClassOrIdSelector.test(parsed.suffix) === false &&
this.isValidSelector(parsed.suffix) === false this.compileSelector(parsed.suffix) === undefined
) { ) {
return true; return true;
} }
@ -895,15 +1016,15 @@ FilterContainer.prototype.compileGenericHideSelector = function(parsed, out) {
return; return;
} }
// Composite CSS rule. // Composite CSS rule.
if ( this.isValidSelector(selector) ) { if ( this.compileSelector(selector) ) {
out.push('c\vlg+\v' + key + '\v' + selector); out.push('c\vlg+\v' + key + '\v' + selector);
} }
return; return;
} }
if ( this.isValidSelector(selector) !== true ) { var compiled = this.compileSelector(selector);
return; if ( compiled === undefined ) { return; }
} // TODO: Detect and error on procedural cosmetic filters.
// ["title"] and ["alt"] will go in high-low generic bin. // ["title"] and ["alt"] will go in high-low generic bin.
if ( this.reHighLow.test(selector) ) { if ( this.reHighLow.test(selector) ) {
@ -948,10 +1069,6 @@ FilterContainer.prototype.compileGenericHideSelector = function(parsed, out) {
FilterContainer.prototype.compileGenericUnhideSelector = function(parsed, out) { FilterContainer.prototype.compileGenericUnhideSelector = function(parsed, out) {
var selector = parsed.suffix; var selector = parsed.suffix;
if ( this.isValidSelector(selector) !== true ) {
return;
}
// script:contains(...) // script:contains(...)
// script:inject(...) // script:inject(...)
if ( this.reScriptSelector.test(selector) ) { if ( this.reScriptSelector.test(selector) ) {
@ -959,10 +1076,14 @@ FilterContainer.prototype.compileGenericUnhideSelector = function(parsed, out) {
return; 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 // https://github.com/chrisaljoudi/uBlock/issues/497
// All generic exception filters are put in the same bucket: they are // All generic exception filters are put in the same bucket: they are
// expected to be very rare. // 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); hostname = this.punycode.toASCII(hostname);
} }
var domain = this.µburi.domainFromHostname(hostname), var selector = parsed.suffix,
domain = this.µburi.domainFromHostname(hostname),
hash; hash;
// script:contains(...) // script:contains(...)
// script:inject(...) // script:inject(...)
if ( this.reScriptSelector.test(parsed.suffix) ) { if ( this.reScriptSelector.test(selector) ) {
hash = domain !== '' ? domain : this.noDomainHash; hash = domain !== '' ? domain : this.noDomainHash;
if ( unhide ) { if ( unhide ) {
hash = '!' + hash; hash = '!' + hash;
} }
out.push('c\vjs\v' + hash + '\v' + hostname + '\v' + parsed.suffix); out.push('c\vjs\v' + hash + '\v' + hostname + '\v' + selector);
return; return;
} }
var compiled = this.compileSelector(selector);
if ( compiled === undefined ) { return; }
// https://github.com/chrisaljoudi/uBlock/issues/188 // https://github.com/chrisaljoudi/uBlock/issues/188
// If not a real domain as per PSL, assign a synthetic one // If not a real domain as per PSL, assign a synthetic one
if ( hostname.endsWith('.*') === false ) { if ( hostname.endsWith('.*') === false ) {
@ -1005,7 +1130,7 @@ FilterContainer.prototype.compileHostnameSelector = function(hostname, parsed, o
hash = '!' + hash; hash = '!' + hash;
} }
out.push('c\vh\v' + hash + '\v' + hostname + '\v' + parsed.suffix); out.push('c\vh\v' + hash + '\v' + hostname + '\v' + compiled);
}; };
/******************************************************************************/ /******************************************************************************/

View File

@ -100,6 +100,10 @@ var onMessage = function(request, sender, callback) {
µb.mouseURL = request.url; µb.mouseURL = request.url;
break; break;
case 'compileCosmeticFilterSelector':
response = µb.cosmeticFilteringEngine.compileSelector(request.selector);
break;
case 'cosmeticFiltersInjected': case 'cosmeticFiltersInjected':
µb.cosmeticFilteringEngine.addToSelectorCache(request); µb.cosmeticFilteringEngine.addToSelectorCache(request);
/* falls through */ /* falls through */

View File

@ -134,14 +134,23 @@ var fromCosmeticFilter = function(details) {
} }
candidates[details.rawFilter] = new RegExp(reStr.join('\\v') + '(?:\\n|$)'); 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. // Second step: find hostname-based versions.
// Reference: FilterContainer.compileHostnameSelector(). // Reference: FilterContainer.compileHostnameSelector().
var pos; var pos,
var hostname = details.hostname; hostname = details.hostname;
if ( hostname !== '' ) { if ( hostname !== '' ) {
for ( ;; ) { for ( ;; ) {
candidates[hostname + '##' + filter] = new RegExp( candidates[hostname + '##' + filter] = new RegExp(
['c', 'h', '[^\\v]+', reEscape(hostname), reEscape(filter)].join('\\v') + ['c', 'h', '[^\\v]+', reEscape(hostname), filterEx].join('\\v') +
'(?:\\n|$)' '(?:\\n|$)'
); );
pos = hostname.indexOf('.'); pos = hostname.indexOf('.');
@ -159,7 +168,7 @@ var fromCosmeticFilter = function(details) {
if ( pos !== -1 ) { if ( pos !== -1 ) {
var entity = domain.slice(0, pos) + '.*'; var entity = domain.slice(0, pos) + '.*';
candidates[entity + '##' + filter] = new RegExp( candidates[entity + '##' + filter] = new RegExp(
['c', 'h', '[^\\v]+', reEscape(entity), reEscape(filter)].join('\\v') + ['c', 'h', '[^\\v]+', reEscape(entity), filterEx].join('\\v') +
'(?:\\n|$)' '(?:\\n|$)'
); );
} }

View File

@ -31,38 +31,36 @@ if ( typeof vAPI !== 'object' || !vAPI.domFilterer ) {
return; return;
} }
var df = vAPI.domFilterer, var loggedSelectors = vAPI.loggedSelectors || {},
loggedSelectors = vAPI.loggedSelectors || {}, matchedSelectors = [];
matchedSelectors = [],
selectors, i, selector;
// CSS selectors. var evaluateSelector = function(selector) {
selectors = df.jobQueue[2]._0.concat(df.jobQueue[3]._0); if (
i = selectors.length; loggedSelectors.hasOwnProperty(selector) === false &&
while ( i-- ) { document.querySelector(selector) !== null
selector = selectors[i]; ) {
if ( loggedSelectors.hasOwnProperty(selector) ) { loggedSelectors[selector] = true;
continue; 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; vAPI.loggedSelectors = loggedSelectors;

View File

@ -693,7 +693,7 @@ var cosmeticFilterMapper = (function() {
i, j; i, j;
// CSS-based selectors: simple one. // CSS-based selectors: simple one.
selectors = vAPI.domFilterer.job2._0; selectors = vAPI.domFilterer.simpleHideSelectors.entries;
i = selectors.length; i = selectors.length;
while ( i-- ) { while ( i-- ) {
selector = selectors[i]; selector = selectors[i];
@ -709,9 +709,9 @@ var cosmeticFilterMapper = (function() {
} }
} }
} }
// CSS-based selectors: complex one (must query from doc root). // CSS-based selectors: complex one (must query from doc root).
selectors = vAPI.domFilterer.job3._0; selectors = vAPI.domFilterer.complexHideSelectors.entries;
i = selectors.length; i = selectors.length;
while ( i-- ) { while ( i-- ) {
selector = selectors[i]; selector = selectors[i];
@ -726,14 +726,12 @@ var cosmeticFilterMapper = (function() {
} }
// Non-CSS selectors. // Non-CSS selectors.
var runJobCallback = function(node, job) { var runJobCallback = function(node, pfilter) {
if ( filterMap.has(node) === false ) { 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.proceduralSelectors.forEachNode(runJobCallback);
vAPI.domFilterer.runJob(vAPI.domFilterer.jobQueue[i], runJobCallback);
}
}; };
var incremental = function(rootNode) { var incremental = function(rootNode) {

View File

@ -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 getElementBoundingClientRect = function(elem) {
var rect = typeof elem.getBoundingClientRect === 'function' ? var rect = typeof elem.getBoundingClientRect === 'function' ?
elem.getBoundingClientRect() : elem.getBoundingClientRect() :
@ -635,7 +643,9 @@ var filtersFrom = function(x, y) {
filterToDOMInterface.set filterToDOMInterface.set
@desc Look-up all the HTML elements matching the filter passed in @desc Look-up all the HTML elements matching the filter passed in
argument. 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. @return array, or undefined if the filter is invalid.
filterToDOMInterface.preview filterToDOMInterface.preview
@ -733,16 +743,15 @@ var filterToDOMInterface = (function() {
// ways to compose a valid href to the same effective URL. One idea is to // 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 // 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. // prefer to refrain from tampering with the page content if I can avoid it.
var fromCosmeticFilter = function(filter) { var fromPlainCosmeticFilter = function(filter) {
var elems; var elems;
try { try {
elems = document.querySelectorAll(filter); elems = document.querySelectorAll(filter);
} }
catch (e) { catch (e) {
return fromProceduralCosmeticFilter(filter); return;
} }
var out = [], var out = [], iElem = elems.length;
iElem = elems.length;
while ( iElem-- ) { while ( iElem-- ) {
out.push({ type: 'cosmetic', elem: elems[iElem]}); out.push({ type: 'cosmetic', elem: elems[iElem]});
} }
@ -751,108 +760,27 @@ var filterToDOMInterface = (function() {
// https://github.com/gorhill/uBlock/issues/1772 // https://github.com/gorhill/uBlock/issues/1772
// Handle procedural cosmetic filters. // Handle procedural cosmetic filters.
var fromProceduralCosmeticFilter = function(filter) { var fromCompiledCosmeticFilter = function(raw) {
if ( filter.charCodeAt(filter.length - 1) === 0x29 /* ')' */ ) { if ( typeof raw !== 'string' ) { return; }
var parts = reProceduralCosmeticFilter.exec(filter); var o;
if ( try {
parts !== null && o = JSON.parse(raw);
proceduralCosmeticFilterFunctions.hasOwnProperty(parts[2]) } catch(ex) {
) { return;
return proceduralCosmeticFilterFunctions[parts[2]](
parts[1].trim(),
parts[3].trim()
);
}
} }
}; var elems;
if ( o.style ) {
var reProceduralCosmeticFilter = /^(.*?):(matches-css|has|style|xpath)\((.+?)\)$/; elems = document.querySelectorAll(o.parts[0]);
lastAction = o.parts.join(' ');
// Collection of handlers for procedural cosmetic filters. } else if ( o.procedural ) {
var proceduralCosmeticFilterFunctions = { elems = vAPI.domFilterer.createProceduralFilter(o).exec();
'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;
} }
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, var lastFilter,
@ -862,26 +790,44 @@ var filterToDOMInterface = (function() {
applied = false, applied = false,
previewing = false; previewing = false;
var queryAll = function(filter) { var queryAll = function(filter, callback) {
filter = filter.trim(); filter = filter.trim();
if ( filter === lastFilter ) { if ( filter === lastFilter ) {
return lastResultset; callback(lastResultset);
return;
} }
unapply(); unapply();
if ( filter === '' ) { if ( filter === '' ) {
lastFilter = ''; lastFilter = '';
lastResultset = []; lastResultset = [];
} else { callback(lastResultset);
lastFilter = filter; return;
lastAction = undefined;
lastResultset = filter.lastIndexOf('##', 0) === 0 ?
fromCosmeticFilter(filter.slice(2)) :
fromNetworkFilter(filter);
if ( previewing ) {
apply(filter);
}
} }
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() { var applyHide = function() {
@ -983,9 +929,11 @@ var filterToDOMInterface = (function() {
var preview = function(filter) { var preview = function(filter) {
previewing = filter !== false; previewing = filter !== false;
if ( previewing ) { if ( previewing ) {
if ( queryAll(filter) !== undefined ) { queryAll(filter, function(items) {
apply(); if ( items !== undefined ) {
} apply();
}
});
} else { } else {
unapply(); unapply();
} }
@ -999,70 +947,75 @@ var filterToDOMInterface = (function() {
}; };
})(); })();
// https://www.youtube.com/watch?v=nuUXJ6RfIik
/******************************************************************************/ /******************************************************************************/
var userFilterFromCandidate = function() { var userFilterFromCandidate = function(callback) {
var v = taCandidate.value; var v = rawFilterFromTextarea();
var items = filterToDOMInterface.set(v); filterToDOMInterface.set(v, function(items) {
if ( !items || items.length === 0 ) { if ( !items || items.length === 0 ) {
return false; callback();
} return;
// 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);
} }
}
pickerBody.querySelector('body section textarea + div').textContent = valid ? // https://github.com/gorhill/uBlock/issues/738
items.length.toLocaleString() : // Trim dots.
'0'; var hostname = window.location.hostname;
taCandidate.classList.toggle('invalidFilter', !valid); if ( hostname.slice(-1) === '.' ) {
dialog.querySelector('#create').disabled = elems.length === 0; hostname = hostname.slice(0, -1);
highlightElements(elems, true); }
// 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 candidateFromFilterChoice = function(filterChoice) {
var slot = filterChoice.slot; var slot = filterChoice.slot;
var filters = filterChoice.filters; var filters = filterChoice.filters;
@ -1132,8 +1085,8 @@ var onDialogClicked = function(ev) {
// We have to exit from preview mode: this guarantees matching elements // We have to exit from preview mode: this guarantees matching elements
// will be found for the candidate filter. // will be found for the candidate filter.
filterToDOMInterface.preview(false); filterToDOMInterface.preview(false);
var filter = userFilterFromCandidate(); userFilterFromCandidate(function(filter) {
if ( filter ) { if ( !filter ) { return; }
var d = new Date(); var d = new Date();
vAPI.messaging.send( vAPI.messaging.send(
'elementPicker', 'elementPicker',
@ -1143,9 +1096,9 @@ var onDialogClicked = function(ev) {
pageDomain: window.location.hostname pageDomain: window.location.hostname
} }
); );
filterToDOMInterface.preview(taCandidate.value); filterToDOMInterface.preview(rawFilterFromTextarea());
stopPicker(); stopPicker();
} });
} }
else if ( ev.target.id === 'pick' ) { else if ( ev.target.id === 'pick' ) {
@ -1161,7 +1114,7 @@ var onDialogClicked = function(ev) {
if ( filterToDOMInterface.previewing() ) { if ( filterToDOMInterface.previewing() ) {
filterToDOMInterface.preview(false); filterToDOMInterface.preview(false);
} else { } else {
filterToDOMInterface.preview(taCandidate.value); filterToDOMInterface.preview(rawFilterFromTextarea());
} }
highlightElements(targetElements, true); highlightElements(targetElements, true);
} }
@ -1300,6 +1253,7 @@ var onKeyPressed = function(ev) {
if ( ev.which === 27 ) { if ( ev.which === 27 ) {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
filterToDOMInterface.preview(false);
stopPicker(); stopPicker();
} }
}; };