mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-06 19:02:30 +01:00
add chainable and recursive cosmetic procedural filters
This commit is contained in:
parent
2f01fcda54
commit
73a69711f2
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
@ -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 */
|
||||
|
@ -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|$)'
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user