Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow :has as native pseudo class #4065

Closed
wants to merge 7 commits into from
Closed
27 changes: 23 additions & 4 deletions packages/adblocker-extended-selectors/src/extended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,22 @@ export enum SelectorType {
Invalid,
}

export function classifySelector(selector: string): SelectorType {
export function classifySelector(
selector: string,
{
extendedPseudoClasses = EXTENDED_PSEUDO_CLASSES,
pseudoClasses = PSEUDO_CLASSES,
pseudoElements = PSEUDO_ELEMENTS,
}: {
extendedPseudoClasses: Set<string>;
pseudoClasses: Set<string>;
pseudoElements: Set<string>;
} = {
extendedPseudoClasses: EXTENDED_PSEUDO_CLASSES,
pseudoClasses: PSEUDO_CLASSES,
pseudoElements: PSEUDO_ELEMENTS,
},
): SelectorType {
// In most cases there is no pseudo-anything so we can quickly exit.
if (selector.indexOf(':') === -1) {
return SelectorType.Normal;
Expand All @@ -109,9 +124,9 @@ export function classifySelector(selector: string): SelectorType {
for (const token of tokens) {
if (token.type === 'pseudo-class') {
const { name } = token;
if (EXTENDED_PSEUDO_CLASSES.has(name) === true) {
if (extendedPseudoClasses.has(name) === true) {
foundSupportedExtendedSelector = true;
} else if (PSEUDO_CLASSES.has(name) === false && PSEUDO_ELEMENTS.has(name) === false) {
} else if (pseudoClasses.has(name) === false && pseudoElements.has(name) === false) {
return SelectorType.Invalid;
}

Expand All @@ -121,7 +136,11 @@ export function classifySelector(selector: string): SelectorType {
token.argument !== undefined &&
RECURSIVE_PSEUDO_CLASSES.has(name) === true
) {
const argumentType = classifySelector(token.argument);
const argumentType = classifySelector(token.argument, {
extendedPseudoClasses,
pseudoClasses,
pseudoElements,
});
if (argumentType === SelectorType.Invalid) {
return argumentType;
} else if (argumentType === SelectorType.Extended) {
Expand Down
7 changes: 6 additions & 1 deletion packages/adblocker/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default class Config {
loadGenericCosmeticsFilters: buffer.getBool(),
loadNetworkFilters: buffer.getBool(),
loadPreprocessors: buffer.getBool(),
useNativePseudoClassHas: buffer.getBool(),
});
}

Expand All @@ -46,6 +47,7 @@ export default class Config {
public readonly loadGenericCosmeticsFilters: boolean;
public readonly loadNetworkFilters: boolean;
public readonly loadPreprocessors: boolean;
public readonly useNativePseudoClassHas: boolean;

constructor({
debug = false,
Expand All @@ -64,6 +66,7 @@ export default class Config {
loadGenericCosmeticsFilters = true,
loadNetworkFilters = true,
loadPreprocessors = false,
useNativePseudoClassHas = false,
}: Partial<Config> = {}) {
this.debug = debug;
this.enableCompression = enableCompression;
Expand All @@ -81,12 +84,13 @@ export default class Config {
this.loadGenericCosmeticsFilters = loadGenericCosmeticsFilters;
this.loadNetworkFilters = loadNetworkFilters;
this.loadPreprocessors = loadPreprocessors;
this.useNativePseudoClassHas = useNativePseudoClassHas;
}

public getSerializedSize(): number {
// NOTE: this should always be the number of attributes and needs to be
// updated when `Config` changes.
return 16 * sizeOfBool();
return 17 * sizeOfBool();
}

public serialize(buffer: StaticDataView): void {
Expand All @@ -106,5 +110,6 @@ export default class Config {
buffer.pushBool(this.loadGenericCosmeticsFilters);
buffer.pushBool(this.loadNetworkFilters);
buffer.pushBool(this.loadPreprocessors);
buffer.pushBool(this.useNativePseudoClassHas);
}
}
29 changes: 27 additions & 2 deletions packages/adblocker/src/filters/cosmetic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
classifySelector,
SelectorType,
parse as parseCssSelector,
PSEUDO_CLASSES,
EXTENDED_PSEUDO_CLASSES,
PSEUDO_ELEMENTS,
} from '@cliqz/adblocker-extended-selectors';

import { Domains } from '../engine/domains.js';
Expand Down Expand Up @@ -184,7 +187,25 @@ export default class CosmeticFilter implements IFilter {
* instance out of it. This function should be *very* efficient, as it will be
* used to parse tens of thousands of lines.
*/
public static parse(line: string, debug: boolean = false): CosmeticFilter | null {
public static parse(
line: string,
{
debug = false,
alternativeExtendedPseudoClasses = EXTENDED_PSEUDO_CLASSES,
alternativePseudoClasses = PSEUDO_CLASSES,
alternativePseudoElements = PSEUDO_ELEMENTS,
}: Partial<{
debug: boolean | undefined;
alternativeExtendedPseudoClasses: Set<string> | undefined;
alternativePseudoClasses: Set<string> | undefined;
alternativePseudoElements: Set<string> | undefined;
}> = {
debug: false,
alternativeExtendedPseudoClasses: EXTENDED_PSEUDO_CLASSES,
alternativePseudoClasses: PSEUDO_CLASSES,
alternativePseudoElements: PSEUDO_ELEMENTS,
},
): CosmeticFilter | null {
const rawLine = line;

// Mask to store attributes. Each flag (unhide, scriptInject, etc.) takes
Expand Down Expand Up @@ -300,7 +321,11 @@ export default class CosmeticFilter implements IFilter {
}
} else {
selector = line.slice(suffixStartIndex);
const selectorType = classifySelector(selector);
const selectorType = classifySelector(selector, {
extendedPseudoClasses: alternativeExtendedPseudoClasses,
pseudoClasses: alternativePseudoClasses,
pseudoElements: alternativePseudoElements,
});
if (selectorType === SelectorType.Extended) {
mask = setBit(mask, COSMETICS_MASK.extended);
} else if (selectorType === SelectorType.Invalid || !isValidCss(selector)) {
Expand Down
29 changes: 26 additions & 3 deletions packages/adblocker/src/lists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

import { EXTENDED_PSEUDO_CLASSES, PSEUDO_CLASSES } from '@cliqz/adblocker-extended-selectors';
import Config from './config.js';
import CosmeticFilter from './filters/cosmetic.js';
import NetworkFilter from './filters/network.js';
Expand Down Expand Up @@ -137,13 +138,23 @@ export function detectFilterType(
return FilterType.NETWORK;
}

export function parseFilter(filter: string): NetworkFilter | CosmeticFilter | null {
export function parseFilter(
filter: string,
config?: Partial<{
alternativeExtendedPseudoClasses: Set<string>;
alternativePseudoClasses: Set<string>;
alternativePseudoElements: Set<string>;
}>,
): NetworkFilter | CosmeticFilter | null {
const filterType = detectFilterType(filter);

if (filterType === FilterType.NETWORK) {
return NetworkFilter.parse(filter, true);
} else if (filterType === FilterType.COSMETIC) {
return CosmeticFilter.parse(filter, true);
return CosmeticFilter.parse(filter, {
...config,
debug: true,
});
}

return null;
Expand All @@ -170,6 +181,14 @@ export function parseFilters(
} {
config = new Config(config);

const alternativeExtendedPseudoClasses = new Set(EXTENDED_PSEUDO_CLASSES);
const alternativePseudoClasses = new Set(PSEUDO_CLASSES);

if (config.useNativePseudoClassHas) {
alternativeExtendedPseudoClasses.delete('has');
alternativePseudoClasses.add('has');
}

const networkFilters: NetworkFilter[] = [];
const cosmeticFilters: CosmeticFilter[] = [];
const notSupportedFilters: NonSupportedFilter[] = [];
Expand Down Expand Up @@ -235,7 +254,11 @@ export function parseFilters(
});
}
} else if (filterType === FilterType.COSMETIC && config.loadCosmeticFilters === true) {
const filter = CosmeticFilter.parse(line, config.debug);
const filter = CosmeticFilter.parse(line, {
debug: config.debug,
alternativeExtendedPseudoClasses,
alternativePseudoClasses,
});
if (filter !== null) {
if (config.loadGenericCosmeticsFilters === true || filter.isGenericHide() === false) {
cosmeticFilters.push(filter);
Expand Down
18 changes: 18 additions & 0 deletions packages/adblocker/test/lists.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,24 @@ describe('#parseFilters', () => {
expect(result).to.have.property('networkFilters').that.have.lengthOf(1);
});
});

context('with useNativePseudoClassHas config', () => {
const config = new Config({
useNativePseudoClassHas: true,
});

it('parses pseudo class has as non-extended', () => {
const result = parseFilters(
`domain.tld##div:has(a)
domain.tld##div:has(a:has(div[class^="test"]))`,
config,
);

for (const filter of result.cosmeticFilters) {
expect(filter.isExtended()).to.be.false;
}
});
});
});

describe('#generateDiff', () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/adblocker/test/parsing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1438,7 +1438,9 @@ const DEFAULT_COSMETIC_FILTER = {
describe('Cosmetic filters', () => {
describe('#toString', () => {
const checkToString = (line: string, expected: string, debug: boolean = false) => {
const parsed = CosmeticFilter.parse(line, debug);
const parsed = CosmeticFilter.parse(line, {
debug,
});
expect(parsed).not.to.be.null;
if (parsed !== null) {
expect(parsed.toString()).to.equal(expected);
Expand Down
Loading