mirror of https://github.com/gorhill/uBlock.git
Add procedural cosmetic operators remove() and upward()
*** New procedural cosmetic operator: `:remove()` Related issue: - https://github.com/gorhill/uBlock/issues/2252 The purpose is to outright remove elements from the DOM tree. Since `:remove()` is an "action" operator, it must only be used as a trailing operator (just like the `:style()` operator). AdGuard's cosmetic filter syntax `{ remove: true; }` will be converted to uBO's `:remove()` operator internally. *** New procedural cosmetic operator: `:upward(...)` The purpose is to lookup an ancestor element. When used with an integer argument, it is synonym of `:nth-ancestor()`, which will be deprecated and which will no longer be supported once no longer used in mainstream filter lists. Filter lists maintainers must only use `:upward(int)` instead of `:nth-ancestor(int)` once the new operator become available in all stable releases of uBO. `:upward()` can also accept a CSS selector as argument, in which case the nearest ancestor which matches the CSS selector will be selected.
This commit is contained in:
parent
14ebfbea27
commit
72bb700568
|
@ -0,0 +1,150 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>CSS selector-based cosmetic filters</title>
|
||||
<style>
|
||||
.filters {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
.tests {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tile {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
margin: 0 20px 10px 0;
|
||||
min-width: 200px;
|
||||
}
|
||||
.tile div {
|
||||
align-items: center;
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.tile > div {
|
||||
height: 50px;
|
||||
position: relative;
|
||||
}
|
||||
.tile > div > div {
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.tile > code {
|
||||
align-self: center;
|
||||
}
|
||||
.pass {
|
||||
background-color: green;
|
||||
}
|
||||
.pass::before {
|
||||
content: 'pass';
|
||||
}
|
||||
.fail {
|
||||
background-color: red;
|
||||
}
|
||||
.fail::before {
|
||||
content: 'fail';
|
||||
}
|
||||
.tests a, .tests b {
|
||||
display: none;
|
||||
}
|
||||
.tests a::before {
|
||||
opacity: 0;
|
||||
}
|
||||
.tests b::after {
|
||||
opacity: 0;
|
||||
}
|
||||
.fail-pseudo::before {
|
||||
align-items: center;
|
||||
background-color: red;
|
||||
content: 'fail';
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>CSS selector-based cosmetic filters</h1>
|
||||
<p><a href="./.">Back</a>
|
||||
<br><br></p>
|
||||
<h3>Filters</h3>
|
||||
<div class="filters"><noscript>Enable JavaScript to see needed filters</noscript></div>
|
||||
|
||||
<h3>Tests</h3>
|
||||
<div id="ccf" class="tests">
|
||||
|
||||
<div id="a1" class="tile">
|
||||
<div class="pass"><div class="fail"><a><b></b></a></div></div>
|
||||
<code class="generic">#ccf #a1 .fail</code>
|
||||
</div>
|
||||
|
||||
<div id="a2" class="tile">
|
||||
<div class="pass"><div class="fail"><a><b></b></a></div></div>
|
||||
<code class="generic">#ccf #a2 .fail:not(.a2)</code>
|
||||
</div>
|
||||
|
||||
<div id="a3" class="tile">
|
||||
<div class="pass"><div class="fail"><a><b></b></a></div></div>
|
||||
<code>#ccf #a3 .fail</code>
|
||||
</div>
|
||||
|
||||
<div id="a4" class="tile">
|
||||
<div class="pass"><div class="fail"><a><b></b></a></div></div>
|
||||
<code>#ccf #a4 .fail:not(.a4)</code>
|
||||
</div>
|
||||
|
||||
<div id="a5" class="tile">
|
||||
<div class="pass"><div class="fail"><a><b></b></a></div></div>
|
||||
<code>#ccf #a5 .fail:style(visibility: hidden)</code>
|
||||
</div>
|
||||
|
||||
<div id="a6" class="tile">
|
||||
<div class="pass"><div class="fail-pseudo"><a><b></b></a></div></div>
|
||||
<code class="generic">#ccf #a6 .fail-pseudo::before</code>
|
||||
</div>
|
||||
|
||||
<div id="a7" class="tile">
|
||||
<div class="pass"><div class="fail-pseudo"><a><b></b></a></div></div>
|
||||
<code>#ccf #a7 .fail-pseudo::before</code>
|
||||
</div>
|
||||
|
||||
<div id="a8" class="tile">
|
||||
<div class="pass"><div class="fail-pseudo"><a><b></b></a></div></div>
|
||||
<code>#ccf #a8 .fail-pseudo::before:style(visibility: hidden)</code>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const hostname = self.location.hostname;
|
||||
const filters = [];
|
||||
const fragment = document.createDocumentFragment();
|
||||
for ( const node of document.querySelectorAll('code') ) {
|
||||
const div = document.createElement('div');
|
||||
let text = '##' + node.textContent;
|
||||
if ( node.classList.contains('generic') === false ) {
|
||||
text = hostname + text;
|
||||
}
|
||||
div.textContent = text;
|
||||
fragment.appendChild(div);
|
||||
}
|
||||
const parent = document.querySelector('.filters');
|
||||
while ( parent.lastElementChild !== null ) {
|
||||
parent.removeChild(parent.lastElementChild);
|
||||
}
|
||||
parent.appendChild(fragment);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -9,12 +9,14 @@
|
|||
<h1>uBlock Origin tests</h1>
|
||||
<p>Some of the pages below are hosted on <a href="raw.githack.com">raw.githack.com</a> in order to ensure some of the secondary resources can be properly loaded (specifically, the WebAssembly modules, as they <a href="https://github.com/WebAssembly/design/blob/master/Web.md#webassemblyinstantiatestreaming">require to be loaded using same-origin policy</a>).</p>
|
||||
<ul>
|
||||
<li><a href="https://raw.githack.com/gorhill/uBlock/master/docs/tests/hntrie-test.html">HNTrie: tests</a>
|
||||
<li><a href="https://raw.githack.com/gorhill/uBlock/master/docs/tests/hnset-benchmark.html">HNTrie, small (2) to medium (~1000) set: benchmarks</a>
|
||||
<li><a href="https://raw.githack.com/gorhill/uBlock/master/docs/tests/hnbigset-benchmark.html">HNTrie, small (2) to large (40,000+) set: benchmarks</a>
|
||||
<li><a href="css-selector-based-cosmetic-filters.html">CSS selector-based cosmetic filters</a>
|
||||
<li><a href="procedural-cosmetic-filters.html">Procedural cosmetic filters</a>
|
||||
<li><a href="procedural-html-filters.html">Procedural HTML filters</a>
|
||||
<li><a href="scriptlet-injection-filters-1.html">Scriptlet injection filters / no-setTimeout-if</a>
|
||||
<li>
|
||||
<li><a href="https://raw.githack.com/gorhill/uBlock/master/docs/tests/hntrie-test.html">HNTrie: tests</a>
|
||||
<li><a href="https://raw.githack.com/gorhill/uBlock/master/docs/tests/hnset-benchmark.html">HNTrie, small (2) to medium (~1000) set: benchmarks</a>
|
||||
<li><a href="https://raw.githack.com/gorhill/uBlock/master/docs/tests/hnbigset-benchmark.html">HNTrie, small (2) to large (40,000+) set: benchmarks</a>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -167,6 +167,21 @@
|
|||
<code>#pcf #a17 .fail:has(~ a:has(b))</code>
|
||||
</div>
|
||||
|
||||
<div id="a18" class="tile">
|
||||
<div class="pass"><div class="fail"></div><a><b></b></a></div>
|
||||
<code>#pcf #a18 .fail:remove()</code>
|
||||
</div>
|
||||
|
||||
<div id="a19" class="tile">
|
||||
<div class="pass"><div class="fail"><a><b></b></a></div></div>
|
||||
<code>#pcf #a19 b:upward(2)</code>
|
||||
</div>
|
||||
|
||||
<div id="a20" class="tile">
|
||||
<div class="pass"><div class="fail"><a><b></b></a></div></div>
|
||||
<code>#pcf #a20 b:upward(.fail)</code>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -138,6 +138,16 @@
|
|||
<code>^#phf #a13 .fail:has(~ a:has(b))</code>
|
||||
</div>
|
||||
|
||||
<div id="a14" class="tile">
|
||||
<div class="pass"><div class="fail"><a><b></b></a></div></div>
|
||||
<code>^#phf #a14 b:upward(2)</code>
|
||||
</div>
|
||||
|
||||
<div id="a15" class="tile">
|
||||
<div class="pass"><div class="fail"><a><b></b></a></div></div>
|
||||
<code>^#phf #a15 b:upward(.fail)</code>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -184,7 +184,7 @@ vAPI.DOMFilterer = class {
|
|||
Array.from(this.specificSimpleHide).join(',\n');
|
||||
}
|
||||
for ( const node of this.addedNodes ) {
|
||||
if ( node[vAPI.matchesProp](this.specificSimpleHideAggregated) ) {
|
||||
if ( node.matches(this.specificSimpleHideAggregated) ) {
|
||||
this.hideNode(node);
|
||||
}
|
||||
const nodes = node.querySelectorAll(this.specificSimpleHideAggregated);
|
||||
|
|
|
@ -413,24 +413,6 @@ vAPI.domWatcher = (( ) => {
|
|||
/******************************************************************************/
|
||||
/******************************************************************************/
|
||||
|
||||
vAPI.matchesProp = (( ) => {
|
||||
const docElem = document.documentElement;
|
||||
if ( typeof docElem.matches !== 'function' ) {
|
||||
if ( typeof docElem.mozMatchesSelector === 'function' ) {
|
||||
return 'mozMatchesSelector';
|
||||
} else if ( typeof docElem.webkitMatchesSelector === 'function' ) {
|
||||
return 'webkitMatchesSelector';
|
||||
} else if ( typeof docElem.msMatchesSelector === 'function' ) {
|
||||
return 'msMatchesSelector';
|
||||
}
|
||||
}
|
||||
return 'matches';
|
||||
})();
|
||||
|
||||
/******************************************************************************/
|
||||
/******************************************************************************/
|
||||
/******************************************************************************/
|
||||
|
||||
vAPI.injectScriptlet = function(doc, text) {
|
||||
if ( !doc ) { return; }
|
||||
let script;
|
||||
|
@ -533,18 +515,10 @@ vAPI.DOMFilterer = (function() {
|
|||
}
|
||||
};
|
||||
|
||||
const PSelectorNthAncestorTask = class {
|
||||
constructor(task) {
|
||||
this.nth = task[1];
|
||||
const PSelectorPassthru = class {
|
||||
constructor() {
|
||||
}
|
||||
transpose(node, output) {
|
||||
let nth = this.nth;
|
||||
for (;;) {
|
||||
node = node.parentElement;
|
||||
if ( node === null ) { return; }
|
||||
nth -= 1;
|
||||
if ( nth === 0 ) { break; }
|
||||
}
|
||||
output.push(node);
|
||||
}
|
||||
};
|
||||
|
@ -571,6 +545,36 @@ vAPI.DOMFilterer = (function() {
|
|||
}
|
||||
};
|
||||
|
||||
const PSelectorUpwardTask = class {
|
||||
constructor(task) {
|
||||
const arg = task[1];
|
||||
if ( typeof arg === 'number' ) {
|
||||
this.i = arg;
|
||||
} else {
|
||||
this.s = arg;
|
||||
}
|
||||
}
|
||||
transpose(node, output) {
|
||||
if ( this.s !== '' ) {
|
||||
const parent = node.parentElement;
|
||||
if ( parent === null ) { return; }
|
||||
node = parent.closest(this.s);
|
||||
if ( node === null ) { return; }
|
||||
} else {
|
||||
let nth = this.i;
|
||||
for (;;) {
|
||||
node = node.parentElement;
|
||||
if ( node === null ) { return; }
|
||||
nth -= 1;
|
||||
if ( nth === 0 ) { break; }
|
||||
}
|
||||
}
|
||||
output.push(node);
|
||||
}
|
||||
};
|
||||
PSelectorUpwardTask.prototype.i = 0;
|
||||
PSelectorUpwardTask.prototype.s = '';
|
||||
|
||||
const PSelectorWatchAttrs = class {
|
||||
constructor(task) {
|
||||
this.observer = null;
|
||||
|
@ -637,8 +641,10 @@ vAPI.DOMFilterer = (function() {
|
|||
[ ':matches-css-before', PSelectorMatchesCSSBeforeTask ],
|
||||
[ ':min-text-length', PSelectorMinTextLengthTask ],
|
||||
[ ':not', PSelectorIfNotTask ],
|
||||
[ ':nth-ancestor', PSelectorNthAncestorTask ],
|
||||
[ ':nth-ancestor', PSelectorUpwardTask ],
|
||||
[ ':remove', PSelectorPassthru ],
|
||||
[ ':spath', PSelectorSpathTask ],
|
||||
[ ':upward', PSelectorUpwardTask ],
|
||||
[ ':watch-attr', PSelectorWatchAttrs ],
|
||||
[ ':xpath', PSelectorXpathTask ],
|
||||
]);
|
||||
|
@ -650,15 +656,21 @@ vAPI.DOMFilterer = (function() {
|
|||
this.selector = o.selector;
|
||||
this.tasks = [];
|
||||
const tasks = o.tasks;
|
||||
if ( !tasks ) { return; }
|
||||
for ( const task of tasks ) {
|
||||
this.tasks.push(new (this.operatorToTaskMap.get(task[0]))(task));
|
||||
if ( Array.isArray(tasks) ) {
|
||||
for ( const task of tasks ) {
|
||||
this.tasks.push(
|
||||
new (this.operatorToTaskMap.get(task[0]))(task)
|
||||
);
|
||||
}
|
||||
}
|
||||
if ( o.action !== undefined ) {
|
||||
this.action = o.action;
|
||||
}
|
||||
}
|
||||
prime(input) {
|
||||
const root = input || document;
|
||||
if ( this.selector === '' ) { return [ root ]; }
|
||||
return root.querySelectorAll(this.selector);
|
||||
return Array.from(root.querySelectorAll(this.selector));
|
||||
}
|
||||
exec(input) {
|
||||
let nodes = this.prime(input);
|
||||
|
@ -689,6 +701,8 @@ vAPI.DOMFilterer = (function() {
|
|||
return false;
|
||||
}
|
||||
};
|
||||
PSelector.prototype.action = undefined;
|
||||
PSelector.prototype.hit = false;
|
||||
PSelector.prototype.operatorToTaskMap = undefined;
|
||||
|
||||
const DOMProceduralFilterer = class {
|
||||
|
@ -707,20 +721,20 @@ vAPI.DOMFilterer = (function() {
|
|||
for ( let i = 0, n = aa.length; i < n; i++ ) {
|
||||
const raw = aa[i];
|
||||
const o = JSON.parse(raw);
|
||||
if ( o.style ) {
|
||||
this.domFilterer.addCSSRule(o.style[0], o.style[1]);
|
||||
if ( o.action === 'style' ) {
|
||||
this.domFilterer.addCSSRule(o.selector, o.tasks[0][1]);
|
||||
mustCommit = true;
|
||||
continue;
|
||||
}
|
||||
if ( o.pseudoclass ) {
|
||||
if ( o.pseudo !== undefined ) {
|
||||
this.domFilterer.addCSSRule(
|
||||
o.raw,
|
||||
o.selector,
|
||||
'display:none!important;'
|
||||
);
|
||||
mustCommit = true;
|
||||
continue;
|
||||
}
|
||||
if ( o.tasks ) {
|
||||
if ( o.tasks !== undefined ) {
|
||||
if ( this.selectors.has(raw) === false ) {
|
||||
const pselector = new PSelector(o);
|
||||
this.selectors.set(raw, pselector);
|
||||
|
@ -757,8 +771,7 @@ vAPI.DOMFilterer = (function() {
|
|||
|
||||
let t0 = Date.now();
|
||||
|
||||
for ( const entry of this.selectors ) {
|
||||
const pselector = entry[1];
|
||||
for ( const pselector of this.selectors.values() ) {
|
||||
const allowance = Math.floor((t0 - pselector.lastAllowanceTime) / 2000);
|
||||
if ( allowance >= 1 ) {
|
||||
pselector.budget += allowance * 50;
|
||||
|
@ -774,9 +787,12 @@ vAPI.DOMFilterer = (function() {
|
|||
pselector.budget = -0x7FFFFFFF;
|
||||
}
|
||||
t0 = t1;
|
||||
for ( const node of nodes ) {
|
||||
this.domFilterer.hideNode(node);
|
||||
this.hiddenNodes.add(node);
|
||||
if ( nodes.length === 0 ) { continue; }
|
||||
pselector.hit = true;
|
||||
if ( pselector.action === 'remove' ) {
|
||||
this.removeNodes(nodes);
|
||||
} else {
|
||||
this.hideNodes(nodes);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -787,6 +803,20 @@ vAPI.DOMFilterer = (function() {
|
|||
//console.timeEnd('procedural selectors/dom layout changed');
|
||||
}
|
||||
|
||||
hideNodes(nodes) {
|
||||
for ( const node of nodes ) {
|
||||
if ( node.parentElement === null ) { continue; }
|
||||
this.domFilterer.hideNode(node);
|
||||
this.hiddenNodes.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
removeNodes(nodes) {
|
||||
for ( const node of nodes ) {
|
||||
node.remove();
|
||||
}
|
||||
}
|
||||
|
||||
createProceduralFilter(o) {
|
||||
return new PSelector(o);
|
||||
}
|
||||
|
|
|
@ -434,7 +434,7 @@ FilterContainer.prototype.compileGenericHideSelector = function(
|
|||
if (
|
||||
compiled === undefined ||
|
||||
compiled !== selector &&
|
||||
µb.staticExtFilteringEngine.compileSelector.pseudoclass !== true
|
||||
µb.staticExtFilteringEngine.compileSelector.pseudoclass === -1
|
||||
) {
|
||||
if ( µb.hiddenSettings.allowGenericProceduralFilters === true ) {
|
||||
return this.compileSpecificSelector('', parsed, writer);
|
||||
|
|
|
@ -95,22 +95,6 @@
|
|||
}
|
||||
};
|
||||
|
||||
const PSelectorNthAncestorTask = class {
|
||||
constructor(task) {
|
||||
this.nth = task[1];
|
||||
}
|
||||
transpose(node, output) {
|
||||
let nth = this.nth;
|
||||
for (;;) {
|
||||
node = node.parentElement;
|
||||
if ( node === null ) { return; }
|
||||
nth -= 1;
|
||||
if ( nth === 0 ) { break; }
|
||||
}
|
||||
output.push(node);
|
||||
}
|
||||
};
|
||||
|
||||
const PSelectorSpathTask = class {
|
||||
constructor(task) {
|
||||
this.spath = task[1];
|
||||
|
@ -133,6 +117,36 @@
|
|||
}
|
||||
};
|
||||
|
||||
const PSelectorUpwardTask = class {
|
||||
constructor(task) {
|
||||
const arg = task[1];
|
||||
if ( typeof arg === 'number' ) {
|
||||
this.i = arg;
|
||||
} else {
|
||||
this.s = arg;
|
||||
}
|
||||
}
|
||||
transpose(node, output) {
|
||||
if ( this.s !== '' ) {
|
||||
const parent = node.parentElement;
|
||||
if ( parent === null ) { return; }
|
||||
node = parent.closest(this.s);
|
||||
if ( node === null ) { return; }
|
||||
} else {
|
||||
let nth = this.i;
|
||||
for (;;) {
|
||||
node = node.parentElement;
|
||||
if ( node === null ) { return; }
|
||||
nth -= 1;
|
||||
if ( nth === 0 ) { break; }
|
||||
}
|
||||
}
|
||||
output.push(node);
|
||||
}
|
||||
};
|
||||
PSelectorUpwardTask.prototype.i = 0;
|
||||
PSelectorUpwardTask.prototype.s = '';
|
||||
|
||||
const PSelectorXpathTask = class {
|
||||
constructor(task) {
|
||||
this.xpe = task[1];
|
||||
|
@ -178,7 +192,7 @@
|
|||
prime(input) {
|
||||
const root = input || docRegister;
|
||||
if ( this.selector === '' ) { return [ root ]; }
|
||||
return root.querySelectorAll(this.selector);
|
||||
return Array.from(root.querySelectorAll(this.selector));
|
||||
}
|
||||
exec(input) {
|
||||
if ( this.invalid ) { return []; }
|
||||
|
@ -218,8 +232,9 @@
|
|||
[ ':if-not', PSelectorIfNotTask ],
|
||||
[ ':min-text-length', PSelectorMinTextLengthTask ],
|
||||
[ ':not', PSelectorIfNotTask ],
|
||||
[ ':nth-ancestor', PSelectorNthAncestorTask ],
|
||||
[ ':nth-ancestor', PSelectorUpwardTask ],
|
||||
[ ':spath', PSelectorSpathTask ],
|
||||
[ ':upward', PSelectorUpwardTask ],
|
||||
[ ':xpath', PSelectorXpathTask ],
|
||||
]);
|
||||
PSelector.prototype.invalid = false;
|
||||
|
@ -246,14 +261,10 @@
|
|||
pselectors.set(selector, pselector);
|
||||
}
|
||||
const nodes = pselector.exec();
|
||||
let i = nodes.length,
|
||||
modified = false;
|
||||
while ( i-- ) {
|
||||
const node = nodes[i];
|
||||
if ( node.parentNode !== null ) {
|
||||
node.parentNode.removeChild(node);
|
||||
modified = true;
|
||||
}
|
||||
let modified = false;
|
||||
for ( const node of nodes ) {
|
||||
node.remove();
|
||||
modified = true;
|
||||
}
|
||||
if ( modified && µb.logger.enabled ) {
|
||||
logOne(details, 0, pselector.raw);
|
||||
|
@ -263,14 +274,10 @@
|
|||
|
||||
const applyCSSSelector = function(details, selector) {
|
||||
const nodes = docRegister.querySelectorAll(selector);
|
||||
let i = nodes.length,
|
||||
modified = false;
|
||||
while ( i-- ) {
|
||||
const node = nodes[i];
|
||||
if ( node.parentNode !== null ) {
|
||||
node.parentNode.removeChild(node);
|
||||
modified = true;
|
||||
}
|
||||
let modified = false;
|
||||
for ( const node of nodes ) {
|
||||
node.remove();
|
||||
modified = true;
|
||||
}
|
||||
if ( modified && µb.logger.enabled ) {
|
||||
logOne(details, 0, selector);
|
||||
|
|
|
@ -147,10 +147,10 @@ const processDeclarativeStyle = function(out) {
|
|||
|
||||
const processProcedural = function(out) {
|
||||
if ( proceduralDict.size === 0 ) { return; }
|
||||
for ( const entry of proceduralDict ) {
|
||||
if ( entry[1].test() === false ) { continue; }
|
||||
out.push(`##${entry[1].raw}`);
|
||||
proceduralDict.delete(entry[0]);
|
||||
for ( const [ raw, pselector ] of proceduralDict ) {
|
||||
if ( pselector.hit === false ) { continue; }
|
||||
out.push(`##${raw}`);
|
||||
proceduralDict.delete(raw);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -822,11 +822,11 @@ const filterToDOMInterface = (( ) => {
|
|||
let elems;
|
||||
try {
|
||||
const o = JSON.parse(raw);
|
||||
if ( o.style ) {
|
||||
if ( o.action === 'style' ) {
|
||||
elems = document.querySelectorAll(
|
||||
o.style[0].replace(rePseudoElements, '')
|
||||
o.selector.replace(rePseudoElements, '')
|
||||
);
|
||||
lastAction = o.style[0] + ' {' + o.style[1] + '}';
|
||||
lastAction = o.selector + ' {' + o.tasks[0][1] + '}';
|
||||
} else if ( o.tasks ) {
|
||||
elems = vAPI.domFilterer.createProceduralFilter(o).exec();
|
||||
}
|
||||
|
|
|
@ -68,46 +68,52 @@
|
|||
parsed.suffix = '';
|
||||
};
|
||||
|
||||
const isValidCSSSelector = (( ) => {
|
||||
const cssPseudoSelector = (( ) => {
|
||||
const rePseudo = /:(?::?after|:?before|:[a-z][a-z-]*[a-z])$/;
|
||||
return function(s) {
|
||||
if ( s.lastIndexOf(':') === -1 ) { return -1; }
|
||||
const match = rePseudo.exec(s);
|
||||
return match !== null ? match.index : -1;
|
||||
};
|
||||
})();
|
||||
|
||||
// Return value:
|
||||
// 0b00 (0) = not a valid CSS selector
|
||||
// 0b01 (1) = valid CSS selector, without pseudo-element
|
||||
// 0b11 (3) = valid CSS selector, with pseudo element
|
||||
const cssSelectorType = (( ) => {
|
||||
const div = document.createElement('div');
|
||||
let 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);
|
||||
}
|
||||
// https://github.com/gorhill/uBlock/issues/3111
|
||||
// Workaround until https://bugzilla.mozilla.org/show_bug.cgi?id=1406817
|
||||
// is fixed.
|
||||
let matchFn;
|
||||
try {
|
||||
matchesFn(':scope');
|
||||
div.matches(':scope');
|
||||
matchFn = div.matches.bind(div);
|
||||
} catch (ex) {
|
||||
matchesFn = div.querySelector.bind(div);
|
||||
matchFn = div.querySelector.bind(div);
|
||||
}
|
||||
// Quick regex-based validation -- most cosmetic filters are of the
|
||||
// simple form and in such case a regex is much faster.
|
||||
const reSimple = /^[#.][\w-]+$/;
|
||||
return s => {
|
||||
if ( reSimple.test(s) ) { return true; }
|
||||
try {
|
||||
matchesFn(`${s}, ${s}:not(#foo)`);
|
||||
} catch (ex) {
|
||||
return false;
|
||||
if ( reSimple.test(s) ) { return 1; }
|
||||
const pos = cssPseudoSelector(s);
|
||||
if ( pos !== -1 ) {
|
||||
return cssSelectorType(s.slice(0, pos)) === 1 ? 3 : 0;
|
||||
}
|
||||
return true;
|
||||
try {
|
||||
matchFn(`${s}, ${s}:not(#foo)`);
|
||||
} catch (ex) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
const isBadRegex = function(s) {
|
||||
try {
|
||||
void new RegExp(s);
|
||||
|
@ -123,12 +129,17 @@
|
|||
if ( matches === null ) { return ''; }
|
||||
const selector = matches[1].trim();
|
||||
const style = matches[2].trim();
|
||||
// Special style directive `remove: true` is converted into a
|
||||
// `:remove()` operator.
|
||||
if ( /^\s*remove:\s*true[; ]*$/.test(style) ) {
|
||||
return `${selector}:remove()`;
|
||||
}
|
||||
// For some reasons, many of Adguard's plain cosmetic filters are
|
||||
// "disguised" as style-based cosmetic filters: convert such filters
|
||||
// to plain cosmetic filters.
|
||||
return /display\s*:\s*none\s*!important;?$/.test(style)
|
||||
? selector
|
||||
: selector + ':style(' + style + ')';
|
||||
: `${selector}:style(${style})`;
|
||||
};
|
||||
|
||||
const hostnamesFromPrefix = function(s) {
|
||||
|
@ -169,6 +180,9 @@
|
|||
'min-text-length',
|
||||
'not',
|
||||
'nth-ancestor',
|
||||
'remove',
|
||||
'style',
|
||||
'upward',
|
||||
'watch-attr',
|
||||
'watch-attrs',
|
||||
'xpath'
|
||||
|
@ -237,9 +251,8 @@
|
|||
const compileInteger = function(s, min = 0, max = 0x7FFFFFFF) {
|
||||
if ( /^\d+$/.test(s) === false ) { return; }
|
||||
const n = parseInt(s, 10);
|
||||
if ( n >= min && n < max ) {
|
||||
return n;
|
||||
}
|
||||
if ( n < min || n >= max ) { return; }
|
||||
return n;
|
||||
};
|
||||
|
||||
const compileNotSelector = function(s) {
|
||||
|
@ -247,21 +260,42 @@
|
|||
// Reject instances of :not() filters for which the argument is
|
||||
// a valid CSS selector, otherwise we would be adversely
|
||||
// changing the behavior of CSS4's :not().
|
||||
if ( isValidCSSSelector(s) === false ) {
|
||||
if ( cssSelectorType(s) === 0 ) {
|
||||
return compileConditionalSelector(s);
|
||||
}
|
||||
};
|
||||
|
||||
const compileNthAncestorSelector = function(s) {
|
||||
return compileInteger(s, 1, 256);
|
||||
const compileUpwardArgument = function(s) {
|
||||
const i = compileInteger(s, 1, 256);
|
||||
if ( i !== undefined ) { return i; }
|
||||
if ( cssSelectorType(s) === 1 ) { return s; }
|
||||
};
|
||||
|
||||
const compileRemoveSelector = function(s) {
|
||||
if ( s === '' ) { return s; }
|
||||
};
|
||||
|
||||
const compileSpathExpression = function(s) {
|
||||
if ( isValidCSSSelector('*' + s) ) {
|
||||
if ( cssSelectorType('*' + s) === 1 ) {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
|
||||
const compileStyleProperties = (( ) => {
|
||||
let div;
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/668
|
||||
return function(s) {
|
||||
if ( /url\(|\\/i.test(s) ) { return; }
|
||||
if ( div === undefined ) {
|
||||
div = document.createElement('div');
|
||||
}
|
||||
div.style.cssText = s;
|
||||
if ( div.style.cssText === '' ) { return; }
|
||||
div.style.cssText = '';
|
||||
return s;
|
||||
};
|
||||
})();
|
||||
|
||||
const compileAttrList = function(s) {
|
||||
const attrs = s.split('\s*,\s*');
|
||||
const out = [];
|
||||
|
@ -287,6 +321,7 @@
|
|||
[ ':-abp-contains', ':has-text' ],
|
||||
[ ':-abp-has', ':has' ],
|
||||
[ ':contains', ':has-text' ],
|
||||
[ ':nth-ancestor', ':upward' ],
|
||||
[ ':watch-attrs', ':watch-attr' ],
|
||||
]);
|
||||
|
||||
|
@ -300,12 +335,19 @@
|
|||
[ ':matches-css-before', compileCSSDeclaration ],
|
||||
[ ':min-text-length', compileInteger ],
|
||||
[ ':not', compileNotSelector ],
|
||||
[ ':nth-ancestor', compileNthAncestorSelector ],
|
||||
[ ':remove', compileRemoveSelector ],
|
||||
[ ':spath', compileSpathExpression ],
|
||||
[ ':style', compileStyleProperties ],
|
||||
[ ':upward', compileUpwardArgument ],
|
||||
[ ':watch-attr', compileAttrList ],
|
||||
[ ':xpath', compileXpathExpression ],
|
||||
]);
|
||||
|
||||
const actionOperators = new Set([
|
||||
':remove',
|
||||
':style',
|
||||
]);
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/2793#issuecomment-333269387
|
||||
// Normalize (somewhat) the stringified version of procedural
|
||||
// cosmetic filters -- this increase the likelihood of detecting
|
||||
|
@ -358,7 +400,9 @@
|
|||
raw.push(task[1]);
|
||||
break;
|
||||
case ':min-text-length':
|
||||
case ':nth-ancestor':
|
||||
case ':remove':
|
||||
case ':style':
|
||||
case ':upward':
|
||||
case ':watch-attr':
|
||||
case ':xpath':
|
||||
raw.push(`${task[0]}(${task[1]})`);
|
||||
|
@ -370,11 +414,14 @@
|
|||
|
||||
const compile = function(raw, root = false) {
|
||||
if ( raw === '' ) { return; }
|
||||
let prefix = '',
|
||||
tasks = [];
|
||||
let i = 0,
|
||||
n = raw.length,
|
||||
opPrefixBeg = 0;
|
||||
|
||||
const tasks = [];
|
||||
const n = raw.length;
|
||||
let prefix = '';
|
||||
let i = 0;
|
||||
let opPrefixBeg = 0;
|
||||
let action;
|
||||
|
||||
for (;;) {
|
||||
let c, match;
|
||||
// Advance to next operator.
|
||||
|
@ -410,45 +457,58 @@
|
|||
if ( pcnt !== 0 && c !== 0x29 ) { return; }
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/341#issuecomment-447603588
|
||||
// Maybe that one operator is a valid CSS selector and if so,
|
||||
// then consider it to be part of the prefix. If there is
|
||||
// at least one task present, then we fail, as we do not
|
||||
// support suffix CSS selectors.
|
||||
if ( isValidCSSSelector(raw.slice(opNameBeg, i)) ) { continue; }
|
||||
// then consider it to be part of the prefix.
|
||||
if ( cssSelectorType(raw.slice(opNameBeg, i)) === 1 ) {
|
||||
continue;
|
||||
}
|
||||
// Extract and remember operator details.
|
||||
let operator = raw.slice(opNameBeg, opNameEnd);
|
||||
operator = normalizedOperators.get(operator) || operator;
|
||||
let args = raw.slice(opNameEnd + 1, i - 1);
|
||||
args = compileArgument.get(operator)(args);
|
||||
// Action operator can only be used as trailing operator in the
|
||||
// root task list.
|
||||
// Per-operator arguments validation
|
||||
const args = compileArgument.get(operator)(
|
||||
raw.slice(opNameEnd + 1, i - 1)
|
||||
);
|
||||
if ( args === undefined ) { return; }
|
||||
if ( opPrefixBeg === 0 ) {
|
||||
prefix = raw.slice(0, opNameBeg);
|
||||
} else if ( opNameBeg !== opPrefixBeg ) {
|
||||
if ( action !== undefined ) { return; }
|
||||
const spath = compileSpathExpression(
|
||||
raw.slice(opPrefixBeg, opNameBeg)
|
||||
);
|
||||
if ( spath === undefined ) { return; }
|
||||
tasks.push([ ':spath', spath ]);
|
||||
}
|
||||
if ( action !== undefined ) { return; }
|
||||
tasks.push([ operator, args ]);
|
||||
if ( actionOperators.has(operator) ) {
|
||||
if ( root === false ) { return; }
|
||||
action = operator.slice(1);
|
||||
}
|
||||
opPrefixBeg = i;
|
||||
if ( i === n ) { break; }
|
||||
}
|
||||
|
||||
// No task found: then we have a CSS selector.
|
||||
// At least one task found: nothing should be left to parse.
|
||||
if ( tasks.length === 0 ) {
|
||||
prefix = raw;
|
||||
} else if ( opPrefixBeg < n ) {
|
||||
if ( action !== undefined ) { return; }
|
||||
const spath = compileSpathExpression(raw.slice(opPrefixBeg));
|
||||
if ( spath === undefined ) { return; }
|
||||
tasks.push([ ':spath', spath ]);
|
||||
}
|
||||
|
||||
// https://github.com/NanoAdblocker/NanoCore/issues/1#issuecomment-354394894
|
||||
// https://www.reddit.com/r/uBlockOrigin/comments/c6iem5/
|
||||
// Convert sibling-selector prefix into :spath operator, but
|
||||
// only if context is not the root.
|
||||
if ( prefix !== '' ) {
|
||||
if ( reIsDanglingSelector.test(prefix) ) { prefix += '*'; }
|
||||
if ( isValidCSSSelector(prefix) === false ) {
|
||||
if ( cssSelectorType(prefix) === 0 ) {
|
||||
if (
|
||||
root ||
|
||||
reIsSiblingSelector.test(prefix) === false ||
|
||||
|
@ -460,10 +520,28 @@
|
|||
prefix = '';
|
||||
}
|
||||
}
|
||||
if ( tasks.length === 0 ) {
|
||||
tasks = undefined;
|
||||
|
||||
const out = { selector: prefix };
|
||||
|
||||
if ( tasks.length !== 0 ) {
|
||||
out.tasks = tasks;
|
||||
}
|
||||
return { selector: prefix, tasks: tasks };
|
||||
|
||||
// Expose action to take in root descriptor.
|
||||
if ( action !== undefined ) {
|
||||
out.action = action;
|
||||
}
|
||||
|
||||
// Pseudo-selectors are valid only when used in a root task list.
|
||||
if ( prefix !== '' ) {
|
||||
const pos = cssPseudoSelector(prefix);
|
||||
if ( pos !== -1 ) {
|
||||
if ( root === false ) { return; }
|
||||
out.pseudo = pos;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
const entryPoint = function(raw) {
|
||||
|
@ -474,7 +552,6 @@
|
|||
let compiled = compile(raw, true);
|
||||
if ( compiled !== undefined ) {
|
||||
compiled.raw = decompile(compiled);
|
||||
compiled = JSON.stringify(compiled);
|
||||
}
|
||||
lastProceduralSelectorCompiled = compiled;
|
||||
return compiled;
|
||||
|
@ -704,11 +781,8 @@
|
|||
// Do not discard unknown pseudo-elements.
|
||||
|
||||
api.compileSelector = (( ) => {
|
||||
const reAfterBeforeSelector = /^(.+?)(::?after|::?before|::[a-z-]+)$/;
|
||||
const reStyleSelector = /^(.+?):style\((.+?)\)$/;
|
||||
const reExtendedSyntax = /\[-(?:abp|ext)-[a-z-]+=(['"])(?:.+?)(?:\1)\]/;
|
||||
const reExtendedSyntaxParser = /\[-(?:abp|ext)-([a-z-]+)=(['"])(.+?)\2\]/;
|
||||
const div = document.createElement('div');
|
||||
|
||||
const normalizedExtendedSyntaxOperators = new Map([
|
||||
[ 'contains', ':has-text' ],
|
||||
|
@ -718,29 +792,17 @@
|
|||
[ 'matches-css-before', ':matches-css-before' ],
|
||||
]);
|
||||
|
||||
const isValidStyleProperty = function(cssText) {
|
||||
if ( reStyleBad.test(cssText) ) { return false; }
|
||||
div.style.cssText = cssText;
|
||||
if ( div.style.cssText === '' ) { return false; }
|
||||
div.style.cssText = '';
|
||||
return true;
|
||||
};
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/668
|
||||
const reStyleBad = /url\(|\\/i;
|
||||
|
||||
const entryPoint = function(raw) {
|
||||
entryPoint.pseudoclass = false;
|
||||
entryPoint.pseudoclass = -1;
|
||||
|
||||
const extendedSyntax = reExtendedSyntax.test(raw);
|
||||
if ( isValidCSSSelector(raw) && extendedSyntax === false ) {
|
||||
if ( cssSelectorType(raw) === 1 && extendedSyntax === false ) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
// We rarely reach this point -- majority of selectors are plain
|
||||
// CSS selectors.
|
||||
|
||||
let matches;
|
||||
|
||||
// Supported Adguard/ABP advanced selector syntax: will translate
|
||||
// into uBO's syntax before further processing.
|
||||
// Mind unsupported advanced selector syntax, such as ABP's
|
||||
|
@ -749,6 +811,7 @@
|
|||
// favor of the procedural one (i.e. `:operator(...)`).
|
||||
// See https://issues.adblockplus.org/ticket/5287
|
||||
if ( extendedSyntax ) {
|
||||
let matches;
|
||||
while ( (matches = reExtendedSyntaxParser.exec(raw)) !== null ) {
|
||||
const operator = normalizedExtendedSyntaxOperators.get(matches[1]);
|
||||
if ( operator === undefined ) { return; }
|
||||
|
@ -759,42 +822,18 @@
|
|||
return entryPoint(raw);
|
||||
}
|
||||
|
||||
let selector = raw, pseudoclass, style;
|
||||
|
||||
// `:style` selector?
|
||||
if ( (matches = reStyleSelector.exec(selector)) !== null ) {
|
||||
selector = matches[1];
|
||||
style = matches[2];
|
||||
}
|
||||
|
||||
// https://github.com/gorhill/uBlock/issues/2448
|
||||
// :after- or :before-based selector?
|
||||
if ( (matches = reAfterBeforeSelector.exec(selector)) ) {
|
||||
selector = matches[1];
|
||||
pseudoclass = matches[2];
|
||||
}
|
||||
|
||||
if ( style !== undefined || pseudoclass !== undefined ) {
|
||||
if ( isValidCSSSelector(selector) === false ) { return; }
|
||||
if ( pseudoclass !== undefined ) {
|
||||
selector += pseudoclass;
|
||||
}
|
||||
if ( style !== undefined ) {
|
||||
if ( isValidStyleProperty(style) === false ) { return; }
|
||||
return JSON.stringify({ raw, style: [ selector, style ] });
|
||||
}
|
||||
entryPoint.pseudoclass = true;
|
||||
return JSON.stringify({ raw, pseudoclass: true });
|
||||
}
|
||||
|
||||
// Procedural selector?
|
||||
const compiled = compileProceduralSelector(raw);
|
||||
if ( compiled !== undefined ) {
|
||||
return compiled;
|
||||
if ( compiled === undefined ) { return; }
|
||||
|
||||
if ( compiled.pseudo !== undefined ) {
|
||||
entryPoint.pseudoclass = compiled.pseudo;
|
||||
}
|
||||
|
||||
return JSON.stringify(compiled);
|
||||
};
|
||||
|
||||
entryPoint.pseudoclass = false;
|
||||
entryPoint.pseudoclass = -1;
|
||||
|
||||
return entryPoint;
|
||||
})();
|
||||
|
|
Loading…
Reference in New Issue