diff --git a/packages/adblocker-extended-selectors/src/extended.ts b/packages/adblocker-extended-selectors/src/extended.ts index ef4263471f..239574c7e0 100644 --- a/packages/adblocker-extended-selectors/src/extended.ts +++ b/packages/adblocker-extended-selectors/src/extended.ts @@ -7,6 +7,7 @@ */ import { tokenize, RECURSIVE_PSEUDO_CLASSES } from './parse.js'; +import { Atoms } from './types.js'; export const EXTENDED_PSEUDO_CLASSES = new Set([ // '-abp-contains', @@ -137,3 +138,28 @@ export function classifySelector(selector: string): SelectorType { return SelectorType.Normal; } + +export function getExtendedPseudoClasses(selector: string): Set { + const extendedSelectors = new Set(); + + if (selector.indexOf(':') === -1) { + return extendedSelectors; + } + + const tokens: Atoms = [...tokenize(selector)]; + + while (tokens.length !== 0) { + const token = tokens.shift()!; + + if (token.type === 'pseudo-class') { + if (EXTENDED_PSEUDO_CLASSES.has(token.name) === true) { + extendedSelectors.add(token.name); + } + if (token.argument !== undefined && RECURSIVE_PSEUDO_CLASSES.has(token.name) === true) { + tokens.push(...tokenize(token.argument)); + } + } + } + + return extendedSelectors; +} diff --git a/packages/adblocker-extended-selectors/src/index.ts b/packages/adblocker-extended-selectors/src/index.ts index db98c037e0..d8902a4261 100644 --- a/packages/adblocker-extended-selectors/src/index.ts +++ b/packages/adblocker-extended-selectors/src/index.ts @@ -15,4 +15,5 @@ export { PSEUDO_ELEMENTS, SelectorType, classifySelector, + getExtendedPseudoClasses, } from './extended.js'; diff --git a/packages/adblocker/src/engine/engine.ts b/packages/adblocker/src/engine/engine.ts index 67c2d802bd..384821821f 100644 --- a/packages/adblocker/src/engine/engine.ts +++ b/packages/adblocker/src/engine/engine.ts @@ -990,6 +990,8 @@ export default class FilterEngine extends EventEmitter { getRulesFromDOM = true, getRulesFromHostname = true, + // Other information + experimentalPseudoClasses = ['has'], hidingStyle, callerContext, }: { @@ -1007,6 +1009,9 @@ export default class FilterEngine extends EventEmitter { getRulesFromDOM?: boolean; getRulesFromHostname?: boolean; + // If set, outputs a separate css block with specified experimental selectors. + // This argument has a higher priority than `getExtendedRules`. + experimentalPseudoClasses?: string[] | undefined; hidingStyle?: string | undefined; callerContext?: any | undefined; }): IMessageFromBackground { @@ -1122,7 +1127,13 @@ export default class FilterEngine extends EventEmitter { applied = true; } } else if (filter.isExtended()) { - if (getExtendedRules === true) { + if ( + experimentalPseudoClasses.length !== 0 && + filter.hasUnsupportedExtendedPseudoClass(experimentalPseudoClasses) === false + ) { + styleFilters.push(filter); + applied = true; + } else if (getExtendedRules === true) { extendedFilters.push(filter); applied = true; } diff --git a/packages/adblocker/src/filters/cosmetic.ts b/packages/adblocker/src/filters/cosmetic.ts index a2ed13650a..78b26c30f6 100644 --- a/packages/adblocker/src/filters/cosmetic.ts +++ b/packages/adblocker/src/filters/cosmetic.ts @@ -11,6 +11,7 @@ import { classifySelector, SelectorType, parse as parseCssSelector, + getExtendedPseudoClasses, } from '@ghostery/adblocker-extended-selectors'; import { Domains } from '../engine/domains.js'; @@ -425,6 +426,7 @@ export default class CosmeticFilter implements IFilter { private id: number | undefined; private scriptletDetails: { name: string; args: string[] } | undefined; + private extendedPseudoClasses: Set | undefined; constructor({ mask, @@ -447,6 +449,7 @@ export default class CosmeticFilter implements IFilter { this.id = undefined; this.rawLine = rawLine; this.scriptletDetails = undefined; + this.extendedPseudoClasses = undefined; } public isCosmeticFilter(): this is CosmeticFilter { @@ -868,6 +871,20 @@ export default class CosmeticFilter implements IFilter { return getBit(this.mask, COSMETICS_MASK.extended); } + public hasUnsupportedExtendedPseudoClass(withPseudoClasses: string[]): boolean { + if (this.extendedPseudoClasses === undefined) { + this.extendedPseudoClasses = getExtendedPseudoClasses(this.getSelector()); + } + + for (const pseudoClass of withPseudoClasses) { + if (!this.extendedPseudoClasses.has(pseudoClass)) { + return true; + } + } + + return false; + } + public isRemove(): boolean { return getBit(this.mask, COSMETICS_MASK.remove); }