Refactor how cosmetic filters with pseudo-elements are parsed

Related issue:
- https://github.com/uBlockOrigin/uBlock-issues/issues/1247#issuecomment-953284365

Distinguish between selectors which can be querySelector-ed
and/or used ni a stylesheet.
This commit is contained in:
Raymond Hill 2021-10-27 18:09:02 -04:00
parent 97a33c9572
commit ef07171f5a
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
2 changed files with 44 additions and 57 deletions

View File

@ -387,7 +387,7 @@ FilterContainer.prototype.compileGenericHideSelector = function(
parser, parser,
writer writer
) { ) {
const { raw, compiled, pseudoclass } = parser.result; const { raw, compiled } = parser.result;
if ( compiled === undefined ) { if ( compiled === undefined ) {
const who = writer.properties.get('name') || '?'; const who = writer.properties.get('name') || '?';
logger.writeOne({ logger.writeOne({
@ -432,7 +432,7 @@ FilterContainer.prototype.compileGenericHideSelector = function(
// https://github.com/uBlockOrigin/uBlock-issues/issues/131 // https://github.com/uBlockOrigin/uBlock-issues/issues/131
// Support generic procedural filters as per advanced settings. // Support generic procedural filters as per advanced settings.
// TODO: prevent double compilation. // TODO: prevent double compilation.
if ( compiled !== raw && pseudoclass === false ) { if ( compiled !== raw ) {
if ( µb.hiddenSettings.allowGenericProceduralFilters === true ) { if ( µb.hiddenSettings.allowGenericProceduralFilters === true ) {
return this.compileSpecificSelector(parser, '', false, writer); return this.compileSpecificSelector(parser, '', false, writer);
} }
@ -1031,11 +1031,6 @@ FilterContainer.prototype.retrieveSpecificSelectors = function(
continue; continue;
} }
} }
if ( pfilter.pseudo !== undefined ) {
injectedHideFilters.push(pfilter.selector);
proceduralSet.delete(json);
continue;
}
} }
if ( proceduralSet.size !== 0 ) { if ( proceduralSet.size !== 0 ) {
out.proceduralFilters = Array.from(proceduralSet); out.proceduralFilters = Array.from(proceduralSet);

View File

@ -129,7 +129,6 @@ const Parser = class {
exception: false, exception: false,
raw: '', raw: '',
compiled: '', compiled: '',
pseudoclass: false,
}; };
this.reset(); this.reset();
} }
@ -289,7 +288,6 @@ const Parser = class {
analyzeExtPattern() { analyzeExtPattern() {
this.result.exception = this.isException(); this.result.exception = this.isException();
this.result.compiled = undefined; this.result.compiled = undefined;
this.result.pseudoclass = false;
let selector = this.strFromSpan(this.patternSpan); let selector = this.strFromSpan(this.patternSpan);
if ( selector === '' ) { if ( selector === '' ) {
@ -1323,7 +1321,11 @@ Parser.prototype.SelectorCompiler = class {
style.remove(); style.remove();
return stylesheet; return stylesheet;
})(); })();
this.rePseudoElement = /:(?::?after|:?before|:-?[a-z][a-z-]*[a-z])$/; this.div = (( ) => {
if ( typeof document !== 'object' ) { return null; }
if ( document instanceof Object === false ) { return null; }
return document.createElement('div');
})();
this.reProceduralOperator = new RegExp([ this.reProceduralOperator = new RegExp([
'^(?:', '^(?:',
Array.from(parser.proceduralOperatorTokens.keys()).join('|'), Array.from(parser.proceduralOperatorTokens.keys()).join('|'),
@ -1366,11 +1368,11 @@ Parser.prototype.SelectorCompiler = class {
} }
let extendedSyntax = false; let extendedSyntax = false;
const selectorType = this.cssSelectorType(raw);
if ( selectorType !== 0 ) { // Can be used in a declarative CSS rule?
if ( this.sheetSelectable(raw) ) {
extendedSyntax = this.reExtendedSyntax.test(raw); extendedSyntax = this.reExtendedSyntax.test(raw);
if ( (extendedSyntax || isProcedural) === false ) { if ( (extendedSyntax || isProcedural) === false ) {
out.pseudoclass = selectorType === 3;
out.compiled = raw; out.compiled = raw;
return true; return true;
} }
@ -1402,9 +1404,6 @@ Parser.prototype.SelectorCompiler = class {
const compiled = this.compileProceduralSelector(raw); const compiled = this.compileProceduralSelector(raw);
if ( compiled === undefined ) { return false; } if ( compiled === undefined ) { return false; }
if ( compiled.pseudo !== undefined ) {
out.pseudoclass = compiled.pseudo;
}
out.compiled = compiled.selector !== compiled.raw out.compiled = compiled.selector !== compiled.raw
? JSON.stringify(compiled) ? JSON.stringify(compiled)
: compiled.selector; : compiled.selector;
@ -1429,11 +1428,6 @@ Parser.prototype.SelectorCompiler = class {
: `${selector}:style(${style})`; : `${selector}:style(${style})`;
} }
// 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
//
// Quick regex-based validation -- most cosmetic filters are of the // Quick regex-based validation -- most cosmetic filters are of the
// simple form and in such case a regex is much faster. // simple form and in such case a regex is much faster.
// Keep in mind: // Keep in mind:
@ -1445,27 +1439,28 @@ Parser.prototype.SelectorCompiler = class {
// https://github.com/uBlockOrigin/uBlock-issues/issues/1751 // https://github.com/uBlockOrigin/uBlock-issues/issues/1751
// Do not rely on matches() or querySelector() to test whether a // Do not rely on matches() or querySelector() to test whether a
// selector is declarative or not. // selector is declarative or not.
cssSelectorType(s) { sheetSelectable(s) {
if ( this.reSimpleSelector.test(s) ) { return 1; } if ( this.reSimpleSelector.test(s) ) { return true; }
const pos = this.cssPseudoElement(s); if ( this.stylesheet === null ) { return true; }
if ( pos !== -1 ) {
return this.cssSelectorType(s.slice(0, pos)) === 1 ? 3 : 0;
}
if ( this.stylesheet === null ) { return 1; }
try { try {
this.stylesheet.insertRule(`${s}{color:red}`); this.stylesheet.insertRule(`${s}{color:red}`);
if ( this.stylesheet.cssRules.length === 0 ) { return 0; } if ( this.stylesheet.cssRules.length === 0 ) { return false; }
this.stylesheet.deleteRule(0); this.stylesheet.deleteRule(0);
} catch (ex) { } catch (ex) {
return 0; return false;
} }
return 1; return true;
} }
cssPseudoElement(s) { querySelectable(s) {
if ( s.lastIndexOf(':') === -1 ) { return -1; } if ( this.reSimpleSelector.test(s) ) { return true; }
const match = this.rePseudoElement.exec(s); if ( this.div === null ) { return true; }
return match !== null ? match.index : -1; try {
this.div.querySelector(`${s},${s}:not(#foo)`);
} catch (ex) {
return false;
}
return true;
} }
compileProceduralSelector(raw) { compileProceduralSelector(raw) {
@ -1534,10 +1529,10 @@ Parser.prototype.SelectorCompiler = class {
// https://github.com/uBlockOrigin/uBlock-issues/issues/341#issuecomment-447603588 // https://github.com/uBlockOrigin/uBlock-issues/issues/341#issuecomment-447603588
// Reject instances of :not() filters for which the argument is // Reject instances of :not() filters for which the argument is
// a valid CSS selector, otherwise we would be adversely // a valid CSS selector, otherwise we would be adversely changing the
// changing the behavior of CSS4's :not(). // behavior of CSS4's :not().
compileNotSelector(s) { compileNotSelector(s) {
if ( this.cssSelectorType(s) === 0 ) { if ( this.querySelectable(s) === false ) {
return this.compileProcedural(s); return this.compileProcedural(s);
} }
} }
@ -1545,7 +1540,7 @@ Parser.prototype.SelectorCompiler = class {
compileUpwardArgument(s) { compileUpwardArgument(s) {
const i = this.compileInteger(s, 1, 256); const i = this.compileInteger(s, 1, 256);
if ( i !== undefined ) { return i; } if ( i !== undefined ) { return i; }
if ( this.cssSelectorType(s) === 1 ) { return s; } if ( this.querySelectable(s) ) { return s; }
} }
compileRemoveSelector(s) { compileRemoveSelector(s) {
@ -1555,7 +1550,7 @@ Parser.prototype.SelectorCompiler = class {
// https://github.com/uBlockOrigin/uBlock-issues/issues/382#issuecomment-703725346 // https://github.com/uBlockOrigin/uBlock-issues/issues/382#issuecomment-703725346
// Prepend `:scope` only when it can be deemed implicit. // Prepend `:scope` only when it can be deemed implicit.
compileSpathExpression(s) { compileSpathExpression(s) {
if ( this.cssSelectorType(/^\s*[+:>~]/.test(s) ? `:scope${s}` : s) === 1 ) { if ( this.querySelectable(/^\s*[+:>~]/.test(s) ? `:scope${s}` : s) ) {
return s; return s;
} }
} }
@ -1714,9 +1709,7 @@ Parser.prototype.SelectorCompiler = class {
// https://github.com/uBlockOrigin/uBlock-issues/issues/341#issuecomment-447603588 // https://github.com/uBlockOrigin/uBlock-issues/issues/341#issuecomment-447603588
// Maybe that one operator is a valid CSS selector and if so, // Maybe that one operator is a valid CSS selector and if so,
// then consider it to be part of the prefix. // then consider it to be part of the prefix.
if ( this.cssSelectorType(raw.slice(opNameBeg, i)) === 1 ) { if ( this.querySelectable(raw.slice(opNameBeg, i)) ) { continue; }
continue;
}
// Extract and remember operator details. // Extract and remember operator details.
let operator = raw.slice(opNameBeg, opNameEnd); let operator = raw.slice(opNameBeg, opNameEnd);
operator = this.normalizedOperators.get(operator) || operator; operator = this.normalizedOperators.get(operator) || operator;
@ -1752,8 +1745,18 @@ Parser.prototype.SelectorCompiler = class {
// No task found: then we have a CSS selector. // No task found: then we have a CSS selector.
// At least one task found: nothing should be left to parse. // At least one task found: nothing should be left to parse.
if ( tasks.length === 0 && action === undefined ) { if ( tasks.length === 0 ) {
prefix = raw; if ( action === undefined ) {
prefix = raw;
}
if ( root && this.sheetSelectable(prefix) ) {
if ( action === undefined ) {
return { selector: prefix };
} else if ( action[0] === ':style' ) {
return { selector: prefix, action };
}
}
} else if ( opPrefixBeg < n ) { } else if ( opPrefixBeg < n ) {
if ( action !== undefined ) { return; } if ( action !== undefined ) { return; }
const spath = this.compileSpathExpression(raw.slice(opPrefixBeg)); const spath = this.compileSpathExpression(raw.slice(opPrefixBeg));
@ -1773,7 +1776,7 @@ Parser.prototype.SelectorCompiler = class {
prefix += ' *'; prefix += ' *';
} }
prefix = prefix.replace(this.reDropScope, ''); prefix = prefix.replace(this.reDropScope, '');
if ( this.cssSelectorType(prefix) === 0 ) { if ( this.querySelectable(prefix) === false ) {
if ( if (
root || root ||
this.reIsCombinator.test(prefix) === false || this.reIsCombinator.test(prefix) === false ||
@ -1797,17 +1800,6 @@ Parser.prototype.SelectorCompiler = class {
out.action = action; out.action = action;
} }
// Pseudo elements are valid only when used in a root task list AND
// only when there are no procedural operators: pseudo elements can't
// be querySelectorAll-ed.
if ( prefix !== '' ) {
const pos = this.cssPseudoElement(prefix);
if ( pos !== -1 ) {
if ( root === false || tasks.length !== 0 ) { return; }
out.pseudo = pos;
}
}
return out; return out;
} }