mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-10 09:07:54 +01:00
Add :remove-attr()
and :remove-class()
pseudo selector operators
These two new pseudo selectors are _action_ operators, and thus can only be used at the end of a selector. They both take as argument a string or regex literal. For `:remove-class()`, when the argument matches a class name, that class name is removed. For `:remove-attr()`, when the argument matches an attribute name, that attribute is removed. These operators are meant to replace `+js(remove-attr, ...)` and `+js(remove-class, ...)`, which from now on are candidate for deprecation in some future. Once the next stable release is widespread, filter authors must use these two new operators instead of their `+js()` counterparts.
This commit is contained in:
parent
e959bc2832
commit
992255e993
4 changed files with 104 additions and 73 deletions
|
@ -176,8 +176,8 @@ const µBlock = { // jshint ignore:line
|
|||
|
||||
// Read-only
|
||||
systemSettings: {
|
||||
compiledMagic: 48, // Increase when compiled format changes
|
||||
selfieMagic: 48, // Increase when selfie format changes
|
||||
compiledMagic: 49, // Increase when compiled format changes
|
||||
selfieMagic: 49, // Increase when selfie format changes
|
||||
},
|
||||
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/759#issuecomment-546654501
|
||||
|
|
|
@ -36,9 +36,6 @@ const nonVisualElements = {
|
|||
|
||||
const regexFromString = (s, exact = false) => {
|
||||
if ( s === '' ) { return /^/; }
|
||||
if ( /^".+"$/.test(s) ) {
|
||||
s = s.slice(1,-1).replace(/\\(\\|")/g, '$1');
|
||||
}
|
||||
const match = /^\/(.+)\/([i]?)$/.exec(s);
|
||||
if ( match !== null ) {
|
||||
return new RegExp(match[1], match[2] || undefined);
|
||||
|
@ -68,11 +65,7 @@ class PSelectorVoidTask extends PSelectorTask {
|
|||
class PSelectorHasTextTask extends PSelectorTask {
|
||||
constructor(task) {
|
||||
super();
|
||||
let arg0 = task[1], arg1;
|
||||
if ( Array.isArray(task[1]) ) {
|
||||
arg1 = arg0[1]; arg0 = arg0[0];
|
||||
}
|
||||
this.needle = new RegExp(arg0, arg1);
|
||||
this.needle = regexFromString(task[1]);
|
||||
}
|
||||
transpose(node, output) {
|
||||
if ( this.needle.test(node.textContent) ) {
|
||||
|
@ -168,11 +161,7 @@ class PSelectorMatchesMediaTask extends PSelectorTask {
|
|||
class PSelectorMatchesPathTask extends PSelectorTask {
|
||||
constructor(task) {
|
||||
super();
|
||||
let arg0 = task[1], arg1;
|
||||
if ( Array.isArray(task[1]) ) {
|
||||
arg1 = arg0[1]; arg0 = arg0[0];
|
||||
}
|
||||
this.needle = new RegExp(arg0, arg1);
|
||||
this.needle = regexFromString(task[1]);
|
||||
}
|
||||
transpose(node, output) {
|
||||
if ( this.needle.test(self.location.pathname + self.location.search) ) {
|
||||
|
@ -450,13 +439,13 @@ class PSelector {
|
|||
PSelector.prototype.operatorToTaskMap = undefined;
|
||||
|
||||
class PSelectorRoot extends PSelector {
|
||||
constructor(o, styleToken) {
|
||||
constructor(o) {
|
||||
super(o);
|
||||
this.budget = 200; // I arbitrary picked a 1/5 second
|
||||
this.raw = o.raw;
|
||||
this.cost = 0;
|
||||
this.lastAllowanceTime = 0;
|
||||
this.styleToken = styleToken;
|
||||
this.action = o.action;
|
||||
}
|
||||
prime(input) {
|
||||
try {
|
||||
|
@ -486,16 +475,8 @@ class ProceduralFilterer {
|
|||
let mustCommit = false;
|
||||
for ( const selector of selectors ) {
|
||||
if ( this.selectors.has(selector.raw) ) { continue; }
|
||||
let style, styleToken;
|
||||
if ( selector.action === undefined ) {
|
||||
style = vAPI.hideStyle;
|
||||
} else if ( selector.action[0] === 'style' ) {
|
||||
style = selector.action[1];
|
||||
}
|
||||
if ( style !== undefined ) {
|
||||
styleToken = this.styleTokenFromStyle(style);
|
||||
}
|
||||
const pselector = new PSelectorRoot(selector, styleToken);
|
||||
const pselector = new PSelectorRoot(selector);
|
||||
this.primeProceduralSelector(pselector);
|
||||
this.selectors.set(selector.raw, pselector);
|
||||
addedSelectors.push(pselector);
|
||||
mustCommit = true;
|
||||
|
@ -510,13 +491,22 @@ class ProceduralFilterer {
|
|||
}
|
||||
}
|
||||
|
||||
// This allows to perform potentially expensive initialization steps
|
||||
// before the filters are ready to be applied.
|
||||
primeProceduralSelector(pselector) {
|
||||
if ( pselector.action === undefined ) {
|
||||
this.styleTokenFromStyle(vAPI.hideStyle);
|
||||
} else if ( pselector.action[0] === 'style' ) {
|
||||
this.styleTokenFromStyle(pselector.action[1]);
|
||||
}
|
||||
return pselector;
|
||||
}
|
||||
|
||||
commitNow() {
|
||||
if ( this.selectors.size === 0 ) { return; }
|
||||
|
||||
this.mustApplySelectors = false;
|
||||
|
||||
//console.time('procedural selectors/dom layout changed');
|
||||
|
||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/341
|
||||
// Be ready to unhide nodes which no longer matches any of
|
||||
// the procedural selectors.
|
||||
|
@ -543,16 +533,15 @@ class ProceduralFilterer {
|
|||
t0 = t1;
|
||||
if ( nodes.length === 0 ) { continue; }
|
||||
pselector.hit = true;
|
||||
this.styleNodes(nodes, pselector.styleToken);
|
||||
this.processNodes(nodes, pselector.action);
|
||||
}
|
||||
|
||||
this.unstyleNodes(toUnstyle);
|
||||
//console.timeEnd('procedural selectors/dom layout changed');
|
||||
this.unprocessNodes(toUnstyle);
|
||||
}
|
||||
|
||||
styleTokenFromStyle(style) {
|
||||
if ( style === undefined ) { return; }
|
||||
let styleToken = this.styleTokenMap.get(style);
|
||||
let styleToken = this.styleTokenMap.get(vAPI.hideStyle);
|
||||
if ( styleToken !== undefined ) { return styleToken; }
|
||||
styleToken = vAPI.randomToken();
|
||||
this.styleTokenMap.set(style, styleToken);
|
||||
|
@ -563,25 +552,60 @@ class ProceduralFilterer {
|
|||
return styleToken;
|
||||
}
|
||||
|
||||
styleNodes(nodes, styleToken) {
|
||||
if ( styleToken === undefined ) {
|
||||
processNodes(nodes, action) {
|
||||
const op = action && action[0] || '';
|
||||
const arg = op !== '' ? action[1] : '';
|
||||
switch ( op ) {
|
||||
case '':
|
||||
/* fall through */
|
||||
case 'style': {
|
||||
const styleToken = this.styleTokenFromStyle(
|
||||
arg === '' ? vAPI.hideStyle : arg
|
||||
);
|
||||
for ( const node of nodes ) {
|
||||
node.textContent = '';
|
||||
node.remove();
|
||||
node.setAttribute(this.masterToken, '');
|
||||
node.setAttribute(styleToken, '');
|
||||
this.styledNodes.add(node);
|
||||
}
|
||||
return;
|
||||
break;
|
||||
}
|
||||
for ( const node of nodes ) {
|
||||
node.setAttribute(this.masterToken, '');
|
||||
node.setAttribute(styleToken, '');
|
||||
this.styledNodes.add(node);
|
||||
case 'remove': {
|
||||
for ( const node of nodes ) {
|
||||
node.remove();
|
||||
node.textContent = '';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'remove-attr': {
|
||||
const reAttr = regexFromString(arg, true);
|
||||
for ( const node of nodes ) {
|
||||
for ( const name of node.getAttributeNames() ) {
|
||||
if ( reAttr.test(name) === false ) { continue; }
|
||||
node.removeAttribute(name);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'remove-class': {
|
||||
const reClass = regexFromString(arg, true);
|
||||
for ( const node of nodes ) {
|
||||
const cl = node.classList;
|
||||
for ( const name of cl.values() ) {
|
||||
if ( reClass.test(name) === false ) { continue; }
|
||||
cl.remove(name);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Current assumption is one style per hit element. Could be an
|
||||
// issue if an element has multiple styling and one styling is
|
||||
// brought back. Possibly too rare to care about this for now.
|
||||
unstyleNodes(nodes) {
|
||||
unprocessNodes(nodes) {
|
||||
for ( const node of nodes ) {
|
||||
if ( this.styledNodes.has(node) ) { continue; }
|
||||
node.removeAttribute(this.masterToken);
|
||||
|
@ -589,7 +613,9 @@ class ProceduralFilterer {
|
|||
}
|
||||
|
||||
createProceduralFilter(o) {
|
||||
return new PSelectorRoot(typeof o === 'string' ? JSON.parse(o) : o);
|
||||
return this.primeProceduralSelector(
|
||||
new PSelectorRoot(typeof o === 'string' ? JSON.parse(o) : o)
|
||||
);
|
||||
}
|
||||
|
||||
onDOMCreated() {
|
||||
|
|
|
@ -756,9 +756,17 @@ const filterToDOMInterface = (( ) => {
|
|||
try {
|
||||
const o = JSON.parse(raw);
|
||||
elems = vAPI.domFilterer.createProceduralFilter(o).exec();
|
||||
style = o.action === undefined || o.action[0] !== 'style'
|
||||
? vAPI.hideStyle
|
||||
: o.action[1];
|
||||
switch ( o.action && o.action[0] || '' ) {
|
||||
case '':
|
||||
case 'remove':
|
||||
style = vAPI.hideStyle;
|
||||
break;
|
||||
case 'style':
|
||||
style = o.action[1];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch(ex) {
|
||||
return;
|
||||
}
|
||||
|
@ -809,6 +817,7 @@ const filterToDOMInterface = (( ) => {
|
|||
const rootElem = document.documentElement;
|
||||
for ( const { elem, style } of lastResultset ) {
|
||||
if ( elem === pickerRoot ) { continue; }
|
||||
if ( style === undefined ) { continue; }
|
||||
if ( elem === rootElem && style === vAPI.hideStyle ) { continue; }
|
||||
let styleToken = vAPI.epickerStyleProxies.get(style);
|
||||
if ( styleToken === undefined ) {
|
||||
|
|
|
@ -1343,7 +1343,6 @@ Parser.prototype.SelectorCompiler = class {
|
|||
|
||||
this.reEatBackslashes = /\\([()])/g;
|
||||
this.reEscapeRegex = /[.*+?^${}()|[\]\\]/g;
|
||||
this.regexToRawValue = new Map();
|
||||
// https://github.com/gorhill/uBlock/issues/2793
|
||||
this.normalizedOperators = new Map([
|
||||
[ '-abp-has', 'has' ],
|
||||
|
@ -1379,6 +1378,8 @@ Parser.prototype.SelectorCompiler = class {
|
|||
]);
|
||||
this.proceduralActionNames = new Set([
|
||||
'remove',
|
||||
'remove-attr',
|
||||
'remove-class',
|
||||
'style',
|
||||
]);
|
||||
this.normalizedExtendedSyntaxOperators = new Map([
|
||||
|
@ -1563,6 +1564,10 @@ Parser.prototype.SelectorCompiler = class {
|
|||
if ( this.maybeProceduralOperatorNames.has(data.name) === false ) {
|
||||
return;
|
||||
}
|
||||
if ( this.astHasType(args, 'ActionSelector') ) {
|
||||
data.type = 'Error';
|
||||
return;
|
||||
}
|
||||
if ( this.astHasType(args, 'ProceduralSelector') ) {
|
||||
data.type = 'ProceduralSelector';
|
||||
return;
|
||||
|
@ -1719,7 +1724,7 @@ Parser.prototype.SelectorCompiler = class {
|
|||
return out.join('');
|
||||
}
|
||||
|
||||
astCompile(parts) {
|
||||
astCompile(parts, details = {}) {
|
||||
if ( Array.isArray(parts) === false ) { return; }
|
||||
if ( parts.length === 0 ) { return; }
|
||||
if ( parts[0].data.type !== 'SelectorList' ) { return; }
|
||||
|
@ -1730,6 +1735,8 @@ Parser.prototype.SelectorCompiler = class {
|
|||
const { data } = part;
|
||||
switch ( data.type ) {
|
||||
case 'ActionSelector': {
|
||||
if ( details.noaction ) { return; }
|
||||
if ( out.action !== undefined ) { return; }
|
||||
if ( prelude.length !== 0 ) {
|
||||
if ( tasks.length === 0 ) {
|
||||
out.selector = prelude.join('');
|
||||
|
@ -1881,23 +1888,20 @@ Parser.prototype.SelectorCompiler = class {
|
|||
compileArgumentAst(operator, parts) {
|
||||
switch ( operator ) {
|
||||
case 'has': {
|
||||
let r = this.astCompile(parts);
|
||||
let r = this.astCompile(parts, { noaction: true });
|
||||
if ( typeof r === 'string' ) {
|
||||
r = { selector: r.replace(/^\s*:scope\s*/, ' ') };
|
||||
}
|
||||
return r;
|
||||
}
|
||||
case 'not': {
|
||||
return this.astCompile(parts);
|
||||
return this.astCompile(parts, { noaction: true });
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
let arg;
|
||||
if ( Array.isArray(parts) && parts.length !== 0 ) {
|
||||
arg = this.astSerialize(parts, false);
|
||||
}
|
||||
if ( Array.isArray(parts) === false || parts.length === 0 ) { return; }
|
||||
const arg = this.astSerialize(parts, false);
|
||||
if ( arg === undefined ) { return; }
|
||||
switch ( operator ) {
|
||||
case 'has-text':
|
||||
|
@ -1924,6 +1928,10 @@ Parser.prototype.SelectorCompiler = class {
|
|||
return this.compileNoArgument(arg);
|
||||
case 'remove':
|
||||
return this.compileNoArgument(arg);
|
||||
case 'remove-attr':
|
||||
return this.compileText(arg);
|
||||
case 'remove-class':
|
||||
return this.compileText(arg);
|
||||
case 'style':
|
||||
return this.compileStyleProperties(arg);
|
||||
case 'upward':
|
||||
|
@ -1999,6 +2007,7 @@ Parser.prototype.SelectorCompiler = class {
|
|||
if ( attr === '' ) { return; }
|
||||
if ( value.length !== 0 ) {
|
||||
r = this.unquoteString(value);
|
||||
if ( r.i !== value.length ) { return; }
|
||||
value = r.s;
|
||||
}
|
||||
return { attr, value };
|
||||
|
@ -2011,22 +2020,7 @@ Parser.prototype.SelectorCompiler = class {
|
|||
if ( s === '' ) { return; }
|
||||
const r = this.unquoteString(s);
|
||||
if ( r.i !== s.length ) { return; }
|
||||
const match = this.reParseRegexLiteral.exec(r.s);
|
||||
let regexDetails;
|
||||
if ( match !== null ) {
|
||||
regexDetails = match[1];
|
||||
if ( this.isBadRegex(regexDetails) ) { return; }
|
||||
if ( match[2] ) {
|
||||
regexDetails = [ regexDetails, match[2] ];
|
||||
}
|
||||
} else if ( r.s === '' ) {
|
||||
regexDetails = '^$';
|
||||
} else {
|
||||
regexDetails = r.s.replace(this.reEatBackslashes, '$1')
|
||||
.replace(this.reEscapeRegex, '\\$&');
|
||||
this.regexToRawValue.set(regexDetails, r.s);
|
||||
}
|
||||
return regexDetails;
|
||||
return r.s;
|
||||
}
|
||||
|
||||
compileCSSDeclaration(s) {
|
||||
|
@ -2051,7 +2045,6 @@ Parser.prototype.SelectorCompiler = class {
|
|||
}
|
||||
} else {
|
||||
regexDetails = '^' + value.replace(this.reEscapeRegex, '\\$&') + '$';
|
||||
this.regexToRawValue.set(regexDetails, value);
|
||||
}
|
||||
return { name, pseudo, value: regexDetails };
|
||||
}
|
||||
|
@ -2138,6 +2131,7 @@ Parser.prototype.proceduralOperatorTokens = new Map([
|
|||
[ 'has-text', 0b01 ],
|
||||
[ 'if', 0b00 ],
|
||||
[ 'if-not', 0b00 ],
|
||||
[ 'matches-attr', 0b01 ],
|
||||
[ 'matches-css', 0b11 ],
|
||||
[ 'matches-media', 0b11 ],
|
||||
[ 'matches-path', 0b11 ],
|
||||
|
@ -2146,6 +2140,8 @@ Parser.prototype.proceduralOperatorTokens = new Map([
|
|||
[ 'nth-ancestor', 0b00 ],
|
||||
[ 'others', 0b01 ],
|
||||
[ 'remove', 0b11 ],
|
||||
[ 'remove-attr', 0b01 ],
|
||||
[ 'remove-class', 0b01 ],
|
||||
[ 'style', 0b11 ],
|
||||
[ 'upward', 0b01 ],
|
||||
[ 'watch-attr', 0b11 ],
|
||||
|
|
Loading…
Reference in a new issue