Convert selector compiler closured code into standalone class

This ensures proper garbage collection once the parser
is no longer referenced -- this is important now that
the parser is instantiated on-demand only.
This commit is contained in:
Raymond Hill 2020-06-13 12:53:49 -04:00
parent 0ec4c911dd
commit a211c2c95d
No known key found for this signature in database
GPG key ID: 25E1490B761470C2

View file

@ -105,6 +105,7 @@ const Parser = class {
this.reIsLocalhostRedirect = /(?:0\.0\.0\.0|(?:broadcast|local)host|local|ip6-\w+)\b/; this.reIsLocalhostRedirect = /(?:0\.0\.0\.0|(?:broadcast|local)host|local|ip6-\w+)\b/;
this.reHostname = /^[^\x00-\x24\x26-\x29\x2B\x2C\x2F\x3A-\x5E\x60\x7B-\x7F]+/; this.reHostname = /^[^\x00-\x24\x26-\x29\x2B\x2C\x2F\x3A-\x5E\x60\x7B-\x7F]+/;
this.punycoder = new URL(self.location); this.punycoder = new URL(self.location);
this.selectorCompiler = new this.SelectorCompiler(this);
// TODO: reuse for network filtering analysis // TODO: reuse for network filtering analysis
this.result = { this.result = {
exception: false, exception: false,
@ -299,7 +300,7 @@ const Parser = class {
this.flavorBits |= BITFlavorExtCosmetic; this.flavorBits |= BITFlavorExtCosmetic;
} }
this.result.raw = selector; this.result.raw = selector;
if ( this.compileSelector(selector, this.result) === false ) { if ( this.selectorCompiler.compile(selector, this.result) === false ) {
this.flavorBits |= BITFlavorUnsupported; this.flavorBits |= BITFlavorUnsupported;
} }
} }
@ -1071,78 +1072,23 @@ const Parser = class {
// https://github.com/uBlockOrigin/uBlock-issues/issues/89 // https://github.com/uBlockOrigin/uBlock-issues/issues/89
// Do not discard unknown pseudo-elements. // Do not discard unknown pseudo-elements.
Parser.prototype.compileSelector = (( ) => { Parser.prototype.SelectorCompiler = class {
const reExtendedSyntax = /\[-(?:abp|ext)-[a-z-]+=(['"])(?:.+?)(?:\1)\]/; constructor(parser) {
const reExtendedSyntaxParser = /\[-(?:abp|ext)-([a-z-]+)=(['"])(.+?)\2\]/; this.parser = parser;
const reParseRegexLiteral = /^\/(.+)\/([imu]+)?$/; this.reExtendedSyntax = /\[-(?:abp|ext)-[a-z-]+=(['"])(?:.+?)(?:\1)\]/;
this.reExtendedSyntaxParser = /\[-(?:abp|ext)-([a-z-]+)=(['"])(.+?)\2\]/;
const translateAdguardCSSInjectionFilter = function(suffix) { this.reParseRegexLiteral = /^\/(.+)\/([imu]+)?$/;
const matches = /^([^{]+)\{([^}]+)\}\s*$/.exec(suffix); this.normalizedExtendedSyntaxOperators = new Map([
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})`;
};
const normalizedExtendedSyntaxOperators = new Map([
[ 'contains', ':has-text' ], [ 'contains', ':has-text' ],
[ 'has', ':has' ], [ 'has', ':has' ],
[ 'matches-css', ':matches-css' ], [ 'matches-css', ':matches-css' ],
[ 'matches-css-after', ':matches-css-after' ], [ 'matches-css-after', ':matches-css-after' ],
[ 'matches-css-before', ':matches-css-before' ], [ 'matches-css-before', ':matches-css-before' ],
]); ]);
this.reSimpleSelector = /^[#.][A-Za-z_][\w-]*$/;
// Return value: this.div = document.createElement('div');
// 0b00 (0) = not a valid CSS selector this.rePseudoClass = /:(?::?after|:?before|:[a-z][a-z-]*[a-z])$/;
// 0b01 (1) = valid CSS selector, without pseudo-element this.reProceduralOperator = new RegExp([
// 0b11 (3) = valid CSS selector, with pseudo element
const cssSelectorType = (( ) => {
// Quick regex-based validation -- most cosmetic filters are of the
// simple form and in such case a regex is much faster.
const reSimple = /^[#.][A-Za-z_][\w-]*$/;
const div = document.createElement('div');
// Keep in mind:
// https://github.com/gorhill/uBlock/issues/693
// https://github.com/gorhill/uBlock/issues/1955
// https://github.com/gorhill/uBlock/issues/3111
// Workaround until https://bugzilla.mozilla.org/show_bug.cgi?id=1406817
// is fixed.
return s => {
if ( reSimple.test(s) ) { return 1; }
const pos = cssPseudoSelector(s);
if ( pos !== -1 ) {
return cssSelectorType(s.slice(0, pos)) === 1 ? 3 : 0;
}
try {
div.matches(`${s}, ${s}:not(#foo)`);
} catch (ex) {
return 0;
}
return 1;
};
})();
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;
};
})();
const compileProceduralSelector = (( ) => {
const reProceduralOperator = new RegExp([
'^(?:', '^(?:',
[ [
'-abp-contains', '-abp-contains',
@ -1167,122 +1113,246 @@ Parser.prototype.compileSelector = (( ) => {
].join('|'), ].join('|'),
')\\(' ')\\('
].join('')); ].join(''));
this.reEatBackslashes = /\\([()])/g;
this.reEscapeRegex = /[.*+?^${}()|[\]\\]/g;
this.reNeedScope = /^\s*>/;
this.reIsDanglingSelector = /[+>~\s]\s*$/;
this.reIsSiblingSelector = /^\s*[+~]/;
this.regexToRawValue = new Map();
// https://github.com/gorhill/uBlock/issues/2793
this.normalizedOperators = new Map([
[ ':-abp-contains', ':has-text' ],
[ ':-abp-has', ':has' ],
[ ':contains', ':has-text' ],
[ ':nth-ancestor', ':upward' ],
[ ':watch-attrs', ':watch-attr' ],
]);
this.actionOperators = new Set([
':remove',
':style',
]);
}
const reEatBackslashes = /\\([()])/g; compile(raw, out) {
const reEscapeRegex = /[.*+?^${}()|[\]\\]/g; // https://github.com/gorhill/uBlock/issues/952
const reNeedScope = /^\s*>/; // Find out whether we are dealing with an Adguard-specific cosmetic
const reIsDanglingSelector = /[+>~\s]\s*$/; // filter, and if so, translate it if supported, or discard it if not
const reIsSiblingSelector = /^\s*[+~]/; // supported.
// We have an Adguard/ABP cosmetic filter if and only if the
// character is `$`, `%` or `?`, otherwise it's not a cosmetic
// filter.
// Adguard's style injection: translate to uBO's format.
if ( hasBits(this.parser.flavorBits, BITFlavorExtStyle) ) {
raw = this.translateAdguardCSSInjectionFilter(raw);
if ( raw === '' ) { return false; }
out.raw = raw;
}
const regexToRawValue = new Map(); let extendedSyntax = false;
const selectorType = this.cssSelectorType(raw);
if ( selectorType !== 0 ) {
extendedSyntax = this.reExtendedSyntax.test(raw);
if ( extendedSyntax === false ) {
out.pseudoclass = selectorType === 3;
out.compiled = raw;
return true;
}
}
const isBadRegex = function(s) { // We rarely reach this point -- majority of selectors are plain
// CSS selectors.
// Supported Adguard/ABP advanced selector syntax: will translate
// into uBO's syntax before further processing.
// Mind unsupported advanced selector syntax, such as ABP's
// `-abp-properties`.
// Note: extended selector syntax has been deprecated in ABP, in
// favor of the procedural one (i.e. `:operator(...)`).
// See https://issues.adblockplus.org/ticket/5287
if ( extendedSyntax ) {
let matches;
while ( (matches = this.reExtendedSyntaxParser.exec(raw)) !== null ) {
const operator = this.normalizedExtendedSyntaxOperators.get(matches[1]);
if ( operator === undefined ) { return false; }
raw = raw.slice(0, matches.index) +
operator + '(' + matches[3] + ')' +
raw.slice(matches.index + matches[0].length);
}
return this.compile(raw, out);
}
// Procedural selector?
const compiled = this.compileProceduralSelector(raw);
if ( compiled === undefined ) { return false; }
if ( compiled.pseudo !== undefined ) {
out.pseudoclass = compiled.pseudo;
}
out.compiled = JSON.stringify(compiled);
return true;
}
translateAdguardCSSInjectionFilter(suffix) {
const matches = /^([^{]+)\{([^}]+)\}\s*$/.exec(suffix);
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})`;
}
// 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
// simple form and in such case a regex is much faster.
// Keep in mind:
// https://github.com/gorhill/uBlock/issues/693
// https://github.com/gorhill/uBlock/issues/1955
// https://github.com/gorhill/uBlock/issues/3111
// Workaround until https://bugzilla.mozilla.org/show_bug.cgi?id=1406817
// is fixed.
cssSelectorType(s) {
if ( this.reSimpleSelector.test(s) ) { return 1; }
const pos = this.cssPseudoSelector(s);
if ( pos !== -1 ) {
return this.cssSelectorType(s.slice(0, pos)) === 1 ? 3 : 0;
}
try {
this.div.matches(`${s}, ${s}:not(#foo)`);
} catch (ex) {
return 0;
}
return 1;
}
cssPseudoSelector(s) {
if ( s.lastIndexOf(':') === -1 ) { return -1; }
const match = this.rePseudoClass.exec(s);
return match !== null ? match.index : -1;
}
compileProceduralSelector(raw) {
const compiled = this.compileProcedural(raw, true);
if ( compiled !== undefined ) {
compiled.raw = this.decompileProcedural(compiled);
}
return compiled;
}
isBadRegex(s) {
try { try {
void new RegExp(s); void new RegExp(s);
} catch (ex) { } catch (ex) {
isBadRegex.message = ex.toString(); this.isBadRegex.message = ex.toString();
return true; return true;
} }
return false; return false;
}; }
// When dealing with literal text, we must first eat _some_ // When dealing with literal text, we must first eat _some_
// backslash characters. // backslash characters.
const compileText = function(s) { compileText(s) {
const match = reParseRegexLiteral.exec(s); const match = this.reParseRegexLiteral.exec(s);
let regexDetails; let regexDetails;
if ( match !== null ) { if ( match !== null ) {
regexDetails = match[1]; regexDetails = match[1];
if ( isBadRegex(regexDetails) ) { return; } if ( this.isBadRegex(regexDetails) ) { return; }
if ( match[2] ) { if ( match[2] ) {
regexDetails = [ regexDetails, match[2] ]; regexDetails = [ regexDetails, match[2] ];
} }
} else { } else {
regexDetails = s.replace(reEatBackslashes, '$1') regexDetails = s.replace(this.reEatBackslashes, '$1')
.replace(reEscapeRegex, '\\$&'); .replace(this.reEscapeRegex, '\\$&');
regexToRawValue.set(regexDetails, s); this.regexToRawValue.set(regexDetails, s);
} }
return regexDetails; return regexDetails;
}; }
const compileCSSDeclaration = function(s) { compileCSSDeclaration(s) {
const pos = s.indexOf(':'); const pos = s.indexOf(':');
if ( pos === -1 ) { return; } if ( pos === -1 ) { return; }
const name = s.slice(0, pos).trim(); const name = s.slice(0, pos).trim();
const value = s.slice(pos + 1).trim(); const value = s.slice(pos + 1).trim();
const match = reParseRegexLiteral.exec(value); const match = this.reParseRegexLiteral.exec(value);
let regexDetails; let regexDetails;
if ( match !== null ) { if ( match !== null ) {
regexDetails = match[1]; regexDetails = match[1];
if ( isBadRegex(regexDetails) ) { return; } if ( this.isBadRegex(regexDetails) ) { return; }
if ( match[2] ) { if ( match[2] ) {
regexDetails = [ regexDetails, match[2] ]; regexDetails = [ regexDetails, match[2] ];
} }
} else { } else {
regexDetails = '^' + value.replace(reEscapeRegex, '\\$&') + '$'; regexDetails = '^' + value.replace(this.reEscapeRegex, '\\$&') + '$';
regexToRawValue.set(regexDetails, value); this.regexToRawValue.set(regexDetails, value);
} }
return { name: name, value: regexDetails }; return { name: name, value: regexDetails };
}; }
const compileConditionalSelector = function(s) { compileConditionalSelector(s) {
// https://github.com/AdguardTeam/ExtendedCss/issues/31#issuecomment-302391277 // https://github.com/AdguardTeam/ExtendedCss/issues/31#issuecomment-302391277
// Prepend `:scope ` if needed. // Prepend `:scope ` if needed.
if ( reNeedScope.test(s) ) { if ( this.reNeedScope.test(s) ) {
s = `:scope ${s}`; s = `:scope ${s}`;
} }
return compile(s); return this.compileProcedural(s);
}; }
const compileInteger = function(s, min = 0, max = 0x7FFFFFFF) { compileInteger(s, min = 0, max = 0x7FFFFFFF) {
if ( /^\d+$/.test(s) === false ) { return; } if ( /^\d+$/.test(s) === false ) { return; }
const n = parseInt(s, 10); const n = parseInt(s, 10);
if ( n < min || n >= max ) { return; } if ( n < min || n >= max ) { return; }
return n; return n;
}; }
const compileNotSelector = function(s) { compileNotSelector(s) {
// 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 behavior of CSS4's :not(). // changing the behavior of CSS4's :not().
if ( cssSelectorType(s) === 0 ) { if ( this.cssSelectorType(s) === 0 ) {
return compileConditionalSelector(s); return this.compileConditionalSelector(s);
}
} }
};
const compileUpwardArgument = function(s) { compileUpwardArgument(s) {
const i = compileInteger(s, 1, 256); const i = this.compileInteger(s, 1, 256);
if ( i !== undefined ) { return i; } if ( i !== undefined ) { return i; }
if ( cssSelectorType(s) === 1 ) { return s; } if ( this.cssSelectorType(s) === 1 ) { return s; }
}; }
const compileRemoveSelector = function(s) { compileRemoveSelector(s) {
if ( s === '' ) { return s; } if ( s === '' ) { return s; }
}; }
const compileSpathExpression = function(s) { compileSpathExpression(s) {
if ( cssSelectorType('*' + s) === 1 ) { if ( this.cssSelectorType('*' + s) === 1 ) {
return s; return s;
} }
}; }
const compileStyleProperties = (( ) => {
let div;
// https://github.com/uBlockOrigin/uBlock-issues/issues/668 // https://github.com/uBlockOrigin/uBlock-issues/issues/668
return function(s) { compileStyleProperties(s) {
if ( /url\(|\\/i.test(s) ) { return; } if ( /url\(|\\/i.test(s) ) { return; }
if ( div === undefined ) { this.div.style.cssText = s;
div = document.createElement('div'); if ( this.div.style.cssText === '' ) { return; }
} this.div.style.cssText = '';
div.style.cssText = s;
if ( div.style.cssText === '' ) { return; }
div.style.cssText = '';
return s; return s;
}; }
})();
const compileAttrList = function(s) { compileAttrList(s) {
const attrs = s.split('\s*,\s*'); const attrs = s.split('\s*,\s*');
const out = []; const out = [];
for ( const attr of attrs ) { for ( const attr of attrs ) {
@ -1291,48 +1361,16 @@ Parser.prototype.compileSelector = (( ) => {
} }
} }
return out; return out;
}; }
const compileXpathExpression = function(s) { compileXpathExpression(s) {
try { try {
document.createExpression(s, null); document.createExpression(s, null);
} catch (e) { } catch (e) {
return; return;
} }
return s; return s;
}; }
// https://github.com/gorhill/uBlock/issues/2793
const normalizedOperators = new Map([
[ ':-abp-contains', ':has-text' ],
[ ':-abp-has', ':has' ],
[ ':contains', ':has-text' ],
[ ':nth-ancestor', ':upward' ],
[ ':watch-attrs', ':watch-attr' ],
]);
const compileArgument = new Map([
[ ':has', compileConditionalSelector ],
[ ':has-text', compileText ],
[ ':if', compileConditionalSelector ],
[ ':if-not', compileConditionalSelector ],
[ ':matches-css', compileCSSDeclaration ],
[ ':matches-css-after', compileCSSDeclaration ],
[ ':matches-css-before', compileCSSDeclaration ],
[ ':min-text-length', compileInteger ],
[ ':not', compileNotSelector ],
[ ':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 // https://github.com/gorhill/uBlock/issues/2793#issuecomment-333269387
// Normalize (somewhat) the stringified version of procedural // Normalize (somewhat) the stringified version of procedural
@ -1341,7 +1379,7 @@ Parser.prototype.compileSelector = (( ) => {
// to other blockers. // to other blockers.
// The normalized string version is what is reported in the logger, // The normalized string version is what is reported in the logger,
// by design. // by design.
const decompile = function(compiled) { decompileProcedural(compiled) {
const tasks = compiled.tasks; const tasks = compiled.tasks;
if ( Array.isArray(tasks) === false ) { if ( Array.isArray(tasks) === false ) {
return compiled.selector; return compiled.selector;
@ -1352,13 +1390,13 @@ Parser.prototype.compileSelector = (( ) => {
switch ( task[0] ) { switch ( task[0] ) {
case ':has': case ':has':
case ':if': case ':if':
raw.push(`:has(${decompile(task[1])})`); raw.push(`:has(${this.decompileProcedural(task[1])})`);
break; break;
case ':has-text': case ':has-text':
if ( Array.isArray(task[1]) ) { if ( Array.isArray(task[1]) ) {
value = `/${task[1][0]}/${task[1][1]}`; value = `/${task[1][0]}/${task[1][1]}`;
} else { } else {
value = regexToRawValue.get(task[1]); value = this.regexToRawValue.get(task[1]);
if ( value === undefined ) { if ( value === undefined ) {
value = `/${task[1]}/`; value = `/${task[1]}/`;
} }
@ -1371,7 +1409,7 @@ Parser.prototype.compileSelector = (( ) => {
if ( Array.isArray(task[1].value) ) { if ( Array.isArray(task[1].value) ) {
value = `/${task[1].value[0]}/${task[1].value[1]}`; value = `/${task[1].value[0]}/${task[1].value[1]}`;
} else { } else {
value = regexToRawValue.get(task[1].value); value = this.regexToRawValue.get(task[1].value);
if ( value === undefined ) { if ( value === undefined ) {
value = `/${task[1].value}/`; value = `/${task[1].value}/`;
} }
@ -1380,7 +1418,7 @@ Parser.prototype.compileSelector = (( ) => {
break; break;
case ':not': case ':not':
case ':if-not': case ':if-not':
raw.push(`:not(${decompile(task[1])})`); raw.push(`:not(${this.decompileProcedural(task[1])})`);
break; break;
case ':spath': case ':spath':
raw.push(task[1]); raw.push(task[1]);
@ -1396,9 +1434,9 @@ Parser.prototype.compileSelector = (( ) => {
} }
} }
return raw.join(''); return raw.join('');
}; }
const compile = function(raw, root = false) { compileProcedural(raw, root = false) {
if ( raw === '' ) { return; } if ( raw === '' ) { return; }
const tasks = []; const tasks = [];
@ -1415,7 +1453,7 @@ Parser.prototype.compileSelector = (( ) => {
while ( i < n ) { while ( i < n ) {
c = raw.charCodeAt(i++); c = raw.charCodeAt(i++);
if ( c === 0x3A /* ':' */ ) { if ( c === 0x3A /* ':' */ ) {
match = reProceduralOperator.exec(raw.slice(i)); match = this.reProceduralOperator.exec(raw.slice(i));
if ( match !== null ) { break; } if ( match !== null ) { break; }
} }
} }
@ -1445,16 +1483,17 @@ Parser.prototype.compileSelector = (( ) => {
// 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 ( cssSelectorType(raw.slice(opNameBeg, i)) === 1 ) { if ( this.cssSelectorType(raw.slice(opNameBeg, i)) === 1 ) {
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 = normalizedOperators.get(operator) || operator; operator = this.normalizedOperators.get(operator) || operator;
// Action operator can only be used as trailing operator in the // Action operator can only be used as trailing operator in the
// root task list. // root task list.
// Per-operator arguments validation // Per-operator arguments validation
const args = compileArgument.get(operator)( const args = this.compileArgument(
operator,
raw.slice(opNameEnd + 1, i - 1) raw.slice(opNameEnd + 1, i - 1)
); );
if ( args === undefined ) { return; } if ( args === undefined ) { return; }
@ -1462,7 +1501,7 @@ Parser.prototype.compileSelector = (( ) => {
prefix = raw.slice(0, opNameBeg); prefix = raw.slice(0, opNameBeg);
} else if ( opNameBeg !== opPrefixBeg ) { } else if ( opNameBeg !== opPrefixBeg ) {
if ( action !== undefined ) { return; } if ( action !== undefined ) { return; }
const spath = compileSpathExpression( const spath = this.compileSpathExpression(
raw.slice(opPrefixBeg, opNameBeg) raw.slice(opPrefixBeg, opNameBeg)
); );
if ( spath === undefined ) { return; } if ( spath === undefined ) { return; }
@ -1470,7 +1509,7 @@ Parser.prototype.compileSelector = (( ) => {
} }
if ( action !== undefined ) { return; } if ( action !== undefined ) { return; }
tasks.push([ operator, args ]); tasks.push([ operator, args ]);
if ( actionOperators.has(operator) ) { if ( this.actionOperators.has(operator) ) {
if ( root === false ) { return; } if ( root === false ) { return; }
action = operator.slice(1); action = operator.slice(1);
} }
@ -1484,7 +1523,7 @@ Parser.prototype.compileSelector = (( ) => {
prefix = raw; prefix = raw;
} else if ( opPrefixBeg < n ) { } else if ( opPrefixBeg < n ) {
if ( action !== undefined ) { return; } if ( action !== undefined ) { return; }
const spath = compileSpathExpression(raw.slice(opPrefixBeg)); const spath = this.compileSpathExpression(raw.slice(opPrefixBeg));
if ( spath === undefined ) { return; } if ( spath === undefined ) { return; }
tasks.push([ ':spath', spath ]); tasks.push([ ':spath', spath ]);
} }
@ -1494,14 +1533,14 @@ Parser.prototype.compileSelector = (( ) => {
// Convert sibling-selector prefix into :spath operator, but // Convert sibling-selector prefix into :spath operator, but
// only if context is not the root. // only if context is not the root.
if ( prefix !== '' ) { if ( prefix !== '' ) {
if ( reIsDanglingSelector.test(prefix) && tasks.length !== 0 ) { if ( this.reIsDanglingSelector.test(prefix) && tasks.length !== 0 ) {
prefix += ' *'; prefix += ' *';
} }
if ( cssSelectorType(prefix) === 0 ) { if ( this.cssSelectorType(prefix) === 0 ) {
if ( if (
root || root ||
reIsSiblingSelector.test(prefix) === false || this.reIsSiblingSelector.test(prefix) === false ||
compileSpathExpression(prefix) === undefined this.compileSpathExpression(prefix) === undefined
) { ) {
return; return;
} }
@ -1529,7 +1568,7 @@ Parser.prototype.compileSelector = (( ) => {
// Pseudo-selectors are valid only when used in a root task list. // Pseudo-selectors are valid only when used in a root task list.
if ( prefix !== '' ) { if ( prefix !== '' ) {
const pos = cssPseudoSelector(prefix); const pos = this.cssPseudoSelector(prefix);
if ( pos !== -1 ) { if ( pos !== -1 ) {
if ( root === false ) { return; } if ( root === false ) { return; }
out.pseudo = pos; out.pseudo = pos;
@ -1537,86 +1576,46 @@ Parser.prototype.compileSelector = (( ) => {
} }
return out; return out;
}
compileArgument(operator, args) {
switch ( operator ) {
case ':has':
return this.compileConditionalSelector(args);
case ':has-text':
return this.compileText(args);
case ':if':
return this.compileConditionalSelector(args);
case ':if-not':
return this.compileConditionalSelector(args);
case ':matches-css':
return this.compileCSSDeclaration(args);
case ':matches-css-after':
return this.compileCSSDeclaration(args);
case ':matches-css-before':
return this.compileCSSDeclaration(args);
case ':min-text-length':
return this.compileInteger(args);
case ':not':
return this.compileNotSelector(args);
case ':remove':
return this.compileRemoveSelector(args);
case ':spath':
return this.compileSpathExpression(args);
case ':style':
return this.compileStyleProperties(args);
case ':upward':
return this.compileUpwardArgument(args);
case ':watch-attr':
return this.compileAttrList(args);
case ':xpath':
return this.compileXpathExpression(args);
default:
break;
}
}
}; };
const entryPoint = function(raw) {
const compiled = compile(raw, true);
if ( compiled !== undefined ) {
compiled.raw = decompile(compiled);
}
return compiled;
};
entryPoint.reset = function() {
regexToRawValue.clear();
};
return entryPoint;
})();
const entryPoint = function(raw, out) {
// https://github.com/gorhill/uBlock/issues/952
// Find out whether we are dealing with an Adguard-specific cosmetic
// filter, and if so, translate it if supported, or discard it if not
// supported.
// We have an Adguard/ABP cosmetic filter if and only if the
// character is `$`, `%` or `?`, otherwise it's not a cosmetic
// filter.
// Adguard's style injection: translate to uBO's format.
if ( hasBits(this.flavorBits, BITFlavorExtStyle) ) {
raw = translateAdguardCSSInjectionFilter(raw);
if ( raw === '' ) { return false; }
out.raw = raw;
}
let extendedSyntax = false;
const selectorType = cssSelectorType(raw);
if ( selectorType !== 0 ) {
extendedSyntax = reExtendedSyntax.test(raw);
if ( extendedSyntax === false ) {
out.pseudoclass = selectorType === 3;
out.compiled = raw;
return true;
}
}
// We rarely reach this point -- majority of selectors are plain
// CSS selectors.
// Supported Adguard/ABP advanced selector syntax: will translate
// into uBO's syntax before further processing.
// Mind unsupported advanced selector syntax, such as ABP's
// `-abp-properties`.
// Note: extended selector syntax has been deprecated in ABP, in
// 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 false; }
raw = raw.slice(0, matches.index) +
operator + '(' + matches[3] + ')' +
raw.slice(matches.index + matches[0].length);
}
return entryPoint.call(this, raw, out);
}
// Procedural selector?
const compiled = compileProceduralSelector(raw);
if ( compiled === undefined ) { return false; }
if ( compiled.pseudo !== undefined ) {
out.pseudoclass = compiled.pseudo;
}
out.compiled = JSON.stringify(compiled);
return true;
};
return entryPoint;
})();
/******************************************************************************/ /******************************************************************************/
const hasNoBits = (v, bits) => (v & bits) === 0; const hasNoBits = (v, bits) => (v & bits) === 0;