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:
Raymond Hill 2022-12-10 11:18:24 -05:00
parent e959bc2832
commit 992255e993
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
4 changed files with 104 additions and 73 deletions

View file

@ -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

View file

@ -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() {

View file

@ -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 ) {

View file

@ -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 ],