mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-11 09:31:01 +01:00
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:
parent
0ec4c911dd
commit
a211c2c95d
1 changed files with 460 additions and 461 deletions
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue