@adguard/agtree
Version:
Tool set for working with adblock filter lists
762 lines (759 loc) • 36.8 kB
JavaScript
/*
* AGTree v4.1.1 (build date: Thu, 23 Apr 2026 09:15:37 GMT)
* (c) 2026 Adguard Software Ltd.
* Released under the MIT license
* https://github.com/AdguardTeam/tsurlfilter/tree/master/packages/agtree#readme
*/
import { sprintf } from 'sprintf-js';
import { cloneDomainListNode } from '../../ast-utils/clone.js';
import { RuleConversionError } from '../../errors/rule-conversion-error.js';
import { AdgHtmlFilteringBodyGenerator } from '../../generator/cosmetic/html-filtering-body/adg-html-filtering-body-generator.js';
import { UboHtmlFilteringBodyGenerator } from '../../generator/cosmetic/html-filtering-body/ubo-html-filtering-body-generator.js';
import { CosmeticRuleSeparator, CosmeticRuleType, RuleCategory } from '../../nodes/index.js';
import { AdgHtmlFilteringBodyParser } from '../../parser/cosmetic/html-filtering-body/adg-html-filtering-body-parser.js';
import { UboHtmlFilteringBodyParser } from '../../parser/cosmetic/html-filtering-body/ubo-html-filtering-body-parser.js';
import { AdblockSyntax } from '../../utils/adblockers.js';
import { EMPTY, EQUALS } from '../../utils/constants.js';
import { RegExpUtils } from '../../utils/regexp.js';
import { createNodeConversionResult } from '../base-interfaces/conversion-result.js';
import { RuleConverterBase } from '../base-interfaces/rule-converter-base.js';
/**
* @file HTML filtering rule converter.
*/
/**
* From the AdGuard docs:
* Specifies the maximum length for content of HTML element. If this parameter is
* set and the content length exceeds the value, a rule does not apply to the element.
* If this parameter is not specified, the max-length is considered to be 8192 (8 KB).
* When converting from other formats, we set the max-length to 262144 (256 KB).
*
* @see {@link https://adguard.com/kb/general/ad-filtering/create-own-filters/#html-filtering-rules}
*/
const ADG_HTML_DEFAULT_MAX_LENGTH = 8192;
const ADG_HTML_CONVERSION_MAX_LENGTH = ADG_HTML_DEFAULT_MAX_LENGTH * 32;
/**
* Supported special pseudo-classes from uBlock.
*
* Note: If new pseudo-classes are added here, ensure to update
* the set and logic in the converter methods accordingly.
*/
const UboPseudoClasses = {
HasText: 'has-text',
MinTextLength: 'min-text-length',
};
/**
* Supported special attribute selectors from AdGuard.
*
* Note: If new pseudo-classes are added here, ensure to update
* the set and logic in the converter methods accordingly.
*/
const AdgAttributeSelectors = {
MaxLength: 'max-length',
MinLength: 'min-length',
TagContent: 'tag-content',
Wildcard: 'wildcard',
};
/**
* Supported special pseudo-classes from AdGuard.
*
* Note: If new pseudo-classes are added here, ensure to update
* the set and logic in the converter methods accordingly.
*/
const AdgPseudoClasses = {
Contains: 'contains',
};
/**
* Set of {@link UboPseudoClasses}.
*/
const SUPPORTED_UBO_PSEUDO_CLASSES = new Set([
UboPseudoClasses.HasText,
UboPseudoClasses.MinTextLength,
]);
/**
* Set of {@link AdgAttributeSelectors}.
*/
const SUPPORTED_ADG_ATTRIBUTE_SELECTORS = new Set([
AdgAttributeSelectors.MaxLength,
AdgAttributeSelectors.MinLength,
AdgAttributeSelectors.TagContent,
AdgAttributeSelectors.Wildcard,
]);
/**
* Set of {@link AdgPseudoClasses}.
*/
const SUPPORTED_ADG_PSEUDO_CLASSES = new Set([
AdgPseudoClasses.Contains,
]);
/**
* Error messages used in HTML filtering rule conversion.
*/
/* eslint-disable max-len */
const ERROR_MESSAGES = {
ABP_NOT_SUPPORTED: 'Invalid rule, ABP does not support HTML filtering rules',
INVALID_RULE: 'Invalid HTML filtering rule: %s',
MIXED_SYNTAX_ADG_UBO: 'Mixed AdGuard and uBlock syntax',
EMPTY_SELECTOR_LIST: 'Selector list of HTML filtering rule must not be empty',
EMPTY_COMPLEX_SELECTOR: 'Complex selector of selector list must not be empty',
INVALID_SELECTOR_COMBINATOR: "Invalid selector combinator '%s' used between selectors",
UNKNOWN_SELECTOR_TYPE: "Unknown selector type '%s' found during conversion",
SPECIAL_ATTRIBUTE_SELECTOR_OPERATOR_INVALID: "Special attribute selector '%s' has invalid operator '%s'",
SPECIAL_ATTRIBUTE_SELECTOR_FLAG_NOT_SUPPORTED: "Special attribute selector '%s' does not support flags",
SPECIAL_ATTRIBUTE_SELECTOR_VALUE_REQUIRED: "Special attribute selector '%s' requires a value",
SPECIAL_ATTRIBUTE_SELECTOR_VALUE_INT: "Value of special attribute selector '%s' must be an integer, got '%s'",
SPECIAL_ATTRIBUTE_SELECTOR_VALUE_POSITIVE: "Value of special attribute selector '%s' must be a positive integer, got '%s'",
SPECIAL_ATTRIBUTE_SELECTOR_NOT_SUPPORTED: "Special attribute selector '%s' is not supported in conversion",
SPECIAL_PSEUDO_CLASS_SELECTOR_ARGUMENT_REQUIRED: "Special pseudo-class selector '%s' requires an argument",
SPECIAL_PSEUDO_CLASS_SELECTOR_ARGUMENT_INT: "Argument of special pseudo-class selector '%s' must be an integer, got '%s'",
SPECIAL_PSEUDO_CLASS_SELECTOR_ARGUMENT_POSITIVE: "Argument of special pseudo-class selector '%s' must be a positive integer, got '%s'",
SPECIAL_PSEUDO_CLASS_SELECTOR_NOT_SUPPORTED: "Special pseudo-class selector '%s' is not supported in conversion",
};
/**
* HTML filtering rule converter class.
*
* @todo Implement `convertToUbo` (ABP currently doesn't support HTML filtering rules).
*/
class HtmlRuleConverter extends RuleConverterBase {
/**
* Converts a HTML rule to AdGuard syntax, if possible.
* Also can be used to convert AdGuard rules to AdGuard syntax to validate them.
*
* @param rule Rule node to convert.
*
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
* If the rule was not converted, the result array will contain the original node with the same object reference.
*
* @throws If the rule is invalid or cannot be converted.
*/
static convertToAdg(rule) {
let parser;
let onSpecialAttributeSelector;
let onSpecialPseudoClassSelector;
let isConverted = false;
if (rule.syntax === AdblockSyntax.Adg) {
parser = AdgHtmlFilteringBodyParser;
onSpecialAttributeSelector = (name, value) => {
/**
* Mark rule as converted in ADG -> ADG conversion only if
* special attribute selectors are present in the rule body,
* because they are deprecated and will be removed soon,
* so we convert them to pseudo-class selectors.
*/
isConverted = true;
return HtmlRuleConverter.convertSpecialAttributeSelectorAdgToAdg(name, value);
};
onSpecialPseudoClassSelector = HtmlRuleConverter.convertSpecialPseudoClassSelectorAdgToAdg;
}
else if (rule.syntax === AdblockSyntax.Ubo) {
/**
* Always mark rule as converted in UBO -> ADG conversion.
*/
isConverted = true;
parser = UboHtmlFilteringBodyParser;
onSpecialAttributeSelector = HtmlRuleConverter.convertSpecialAttributeSelectorUboToAdg;
onSpecialPseudoClassSelector = HtmlRuleConverter.convertSpecialPseudoClassSelectorUboToAdg;
}
else {
throw new RuleConversionError(ERROR_MESSAGES.ABP_NOT_SUPPORTED);
}
// Convert body
const convertedBody = HtmlRuleConverter.convertBody(rule.body, parser, AdgHtmlFilteringBodyGenerator, onSpecialAttributeSelector, onSpecialPseudoClassSelector, rule.syntax === AdblockSyntax.Adg);
if (!isConverted) {
return createNodeConversionResult([rule], false);
}
return createNodeConversionResult([{
category: RuleCategory.Cosmetic,
type: CosmeticRuleType.HtmlFilteringRule,
syntax: AdblockSyntax.Adg,
exception: rule.exception,
domains: cloneDomainListNode(rule.domains),
// Convert the separator based on the exception status
separator: {
type: 'Value',
value: rule.exception
? CosmeticRuleSeparator.AdgHtmlFilteringException
: CosmeticRuleSeparator.AdgHtmlFiltering,
},
body: convertedBody,
}], true);
}
/**
* Converts a HTML rule to uBlock syntax, if possible.
* Also can be used to convert uBlock rules to uBlock syntax to validate them.
*
* @param rule Rule node to convert.
*
* @returns An object which follows the {@link NodeConversionResult} interface. Its `result` property contains
* the array of converted rule nodes, and its `isConverted` flag indicates whether the original rule was converted.
* If the rule was not converted, the result array will contain the original node with the same object reference.
*
* @throws Error if the rule is invalid or cannot be converted.
*/
static convertToUbo(rule) {
// Ignore uBlock rules
if (rule.syntax === AdblockSyntax.Ubo) {
return createNodeConversionResult([rule], false);
}
if (rule.syntax === AdblockSyntax.Abp) {
throw new RuleConversionError(ERROR_MESSAGES.ABP_NOT_SUPPORTED);
}
// Convert body
const convertedBody = HtmlRuleConverter.convertBody(rule.body, AdgHtmlFilteringBodyParser, UboHtmlFilteringBodyGenerator, HtmlRuleConverter.convertSpecialAttributeSelectorAdgToUbo, HtmlRuleConverter.convertSpecialPseudoClassSelectorAdgToUbo);
return createNodeConversionResult([{
category: RuleCategory.Cosmetic,
type: CosmeticRuleType.HtmlFilteringRule,
syntax: AdblockSyntax.Ubo,
exception: rule.exception,
domains: cloneDomainListNode(rule.domains),
separator: {
type: 'Value',
value: rule.exception
? CosmeticRuleSeparator.ElementHidingException
: CosmeticRuleSeparator.ElementHiding,
},
body: convertedBody,
}], true);
}
/**
* Handles special attribute selectors during AdGuard to AdGuard conversion:
* - `[tag-content="content"]` -> `:contains(content)`
* direct conversion, no changes to value
* - `[wildcard="*content*"]` -> `:contains(/*.content*./s)`
* convert search pattern to regular expression
* - `[min-length="min"]` -> `:contains(/^(?=.{min,}$).*\/s)`
* converts to a length-matching regular expression.
* - `[max-length="max"]` -> `:contains(/^(?=.{0,max}$).*\/s)`
* converts to a length-matching regular expression.
*
* Note: This attribute selector to pseudo-class selector conversion
* is needed because AdGuard special attribute selectors are going
* to be deprecated and removed soon.
*
* @param name Name of the special attribute selector.
* @param value Value of the special attribute selector.
*
* @returns A {@link SimpleSelector} to add to the current complex selector.
*/
static convertSpecialAttributeSelectorAdgToAdg(name, value) {
switch (name) {
// `[tag-content="content"]` -> `:contains(content)`
// direct conversion, no changes to value
case AdgAttributeSelectors.TagContent: {
return HtmlRuleConverter.getPseudoClassSelectorNode(AdgPseudoClasses.Contains, value);
}
// `[wildcard="*content*"] -> `:contains(/*.content*./s)`
// convert search pattern to regular expression
case AdgAttributeSelectors.Wildcard: {
return HtmlRuleConverter.getPseudoClassSelectorNode(AdgPseudoClasses.Contains, RegExpUtils.globToRegExp(value));
}
// `[min-length="min"]` -> `:contains(/^(?=.{min,}$).*\/s)`
// `[max-length="max"]` -> `:contains(/^(?=.{0,max}$).*\/s)`
// converts to a length-matching regular expression
case AdgAttributeSelectors.MinLength:
case AdgAttributeSelectors.MaxLength: {
// Validate length value
HtmlRuleConverter.assertValidLengthValue(name, value, ERROR_MESSAGES.SPECIAL_ATTRIBUTE_SELECTOR_VALUE_INT, ERROR_MESSAGES.SPECIAL_ATTRIBUTE_SELECTOR_VALUE_POSITIVE);
// It's safe to cast to number here after validation
const length = Number(value);
let min = null;
let max = null;
if (name === AdgAttributeSelectors.MinLength) {
min = length;
}
else {
max = length;
}
return HtmlRuleConverter.getPseudoClassSelectorNode(AdgPseudoClasses.Contains, RegExpUtils.getLengthRegexp(min, max));
}
// This line is unreachable due to exhausted cases, but we keep it to satisfy TS
default: {
throw new RuleConversionError(sprintf(ERROR_MESSAGES.SPECIAL_ATTRIBUTE_SELECTOR_NOT_SUPPORTED, name));
}
}
}
/**
* Since special pseudo-class selectors do not need conversion
* in AdGuard to AdGuard conversion, we simply return `true` to keep them as-is.
*
* @param name Name of the special pseudo-class selector.
*
* @returns `true` to keep the special pseudo-class selector as-is.
*
* @throws Rule conversion error for mixed syntax.
*/
static convertSpecialPseudoClassSelectorAdgToAdg(name) {
if (SUPPORTED_UBO_PSEUDO_CLASSES.has(name)) {
throw new RuleConversionError(sprintf(ERROR_MESSAGES.INVALID_RULE, ERROR_MESSAGES.MIXED_SYNTAX_ADG_UBO));
}
return true;
}
/**
* Since special attribute selectors only AdGuard-specific,
* we should never encounter them in uBlock rules.
*
* @throws Rule conversion error for mixed syntax.
*/
static convertSpecialAttributeSelectorUboToAdg() {
throw new RuleConversionError(sprintf(ERROR_MESSAGES.INVALID_RULE, ERROR_MESSAGES.MIXED_SYNTAX_ADG_UBO));
}
/**
* Handles special pseudo-class selectors during uBlock to AdGuard conversion:
* - `:has-text(text)` -> `:contains(text)`
* direct conversion, no changes to argument
* - `:min-text-length(min)` -> `:contains(/^(?=.{min,MAX_CONVERSION_DEFAULT}$).*\/s)`
* converts to a length-matching regular expression.
*
* @param name Name of the special pseudo-class selector.
* @param argument Argument of the special pseudo-class selector.
*
* @returns A {@link SimpleSelector} to add to the current complex selector.
*
* @throws If AdGuard-specific pseudo-class selector is found in uBlock rule.
*/
static convertSpecialPseudoClassSelectorUboToAdg(name, argument) {
switch (name) {
// `:has-text(text)` -> `:contains(text)`
// direct conversion, no changes to argument
case UboPseudoClasses.HasText: {
return HtmlRuleConverter.getPseudoClassSelectorNode(AdgPseudoClasses.Contains, argument);
}
// `:min-text-length(min)` -> `:contains(/^(?=.{min,MAX_CONVERSION_DEFAULT}$).*\/s)`
// converts to a length-matching regular expression
case UboPseudoClasses.MinTextLength: {
// Validate length value
HtmlRuleConverter.assertValidLengthValue(name, argument, ERROR_MESSAGES.SPECIAL_PSEUDO_CLASS_SELECTOR_ARGUMENT_INT, ERROR_MESSAGES.SPECIAL_PSEUDO_CLASS_SELECTOR_ARGUMENT_POSITIVE);
// It's safe to cast to number here after validation
const minLength = Number(argument);
return HtmlRuleConverter.getPseudoClassSelectorNode(AdgPseudoClasses.Contains, RegExpUtils.getLengthRegexp(minLength, ADG_HTML_CONVERSION_MAX_LENGTH));
}
// Throw an error if the AdGuard-specific pseudo-class selector found in uBlock rule
case AdgPseudoClasses.Contains: {
throw new RuleConversionError(sprintf(ERROR_MESSAGES.INVALID_RULE, ERROR_MESSAGES.MIXED_SYNTAX_ADG_UBO));
}
// This line is unreachable due to exhausted cases, but we keep it to satisfy TS
default: {
throw new RuleConversionError(sprintf(ERROR_MESSAGES.SPECIAL_PSEUDO_CLASS_SELECTOR_NOT_SUPPORTED, name));
}
}
}
/**
* Handles special attribute selectors during AdGuard to uBlock conversion:
* - `[tag-content="content"]` -> `:has-text(content)`
* direct conversion, no changes to value
* - `[wildcard="*content*"]` -> `:has-text(/*.content*./s)`
* convert search pattern to regular expression
* - `[min-length="min"]` -> `:min-text-length(min)`
* direct conversion, no changes to value
* - `[max-length]` is skipped.
*
* @param name Name of the special attribute selector.
* @param value Value of the special attribute selector.
*
* @returns A {@link SimpleSelector} to add to the current complex selector, or `false` to skip it.
*/
static convertSpecialAttributeSelectorAdgToUbo(name, value) {
switch (name) {
// `[tag-content="content"]` -> `:has-text(content)`
// direct conversion, no changes to value
case AdgAttributeSelectors.TagContent: {
return HtmlRuleConverter.getPseudoClassSelectorNode(UboPseudoClasses.HasText, value);
}
// `[wildcard="*content*"] -> `:has-text(/*.content*./s)`
// convert search pattern to regular expression
case AdgAttributeSelectors.Wildcard: {
return HtmlRuleConverter.getPseudoClassSelectorNode(UboPseudoClasses.HasText, RegExpUtils.globToRegExp(value));
}
// `[min-length="min"]` -> `:min-text-length(min)`
// direct conversion, no changes to value
case AdgAttributeSelectors.MinLength: {
// Validate length value
HtmlRuleConverter.assertValidLengthValue(name, value, ERROR_MESSAGES.SPECIAL_ATTRIBUTE_SELECTOR_VALUE_INT, ERROR_MESSAGES.SPECIAL_ATTRIBUTE_SELECTOR_VALUE_POSITIVE);
return HtmlRuleConverter.getPseudoClassSelectorNode(UboPseudoClasses.MinTextLength, value);
}
// `[max-length]` is skipped
case AdgAttributeSelectors.MaxLength: {
return false;
}
// This line is unreachable due to exhausted cases, but we keep it to satisfy TS
default: {
throw new RuleConversionError(sprintf(ERROR_MESSAGES.SPECIAL_ATTRIBUTE_SELECTOR_NOT_SUPPORTED, name));
}
}
}
/**
* Handles special pseudo-class selectors during AdGuard to uBlock conversion:
* - `:contains(text)` -> `:has-text(text)`
* direct conversion, no changes to argument.
*
* @param name Name of the special pseudo-class selector.
* @param argument Argument of the special pseudo-class selector.
*
* @returns A {@link SimpleSelector} to add to the current complex selector.
*
* @throws If uBlock-specific pseudo-class selector is found in AdGuard rule.
*/
static convertSpecialPseudoClassSelectorAdgToUbo(name, argument) {
switch (name) {
// `:contains(text)` -> `:has-text(text)`
// direct conversion, no changes to argument
case AdgPseudoClasses.Contains: {
return HtmlRuleConverter.getPseudoClassSelectorNode(UboPseudoClasses.HasText, argument);
}
// Throw an error if the uBlock-specific pseudo-class selector found in AdGuard rule
case UboPseudoClasses.HasText:
case UboPseudoClasses.MinTextLength: {
throw new RuleConversionError(sprintf(ERROR_MESSAGES.INVALID_RULE, ERROR_MESSAGES.MIXED_SYNTAX_ADG_UBO));
}
// This line is unreachable due to exhausted cases, but we keep it to satisfy TS
default: {
throw new RuleConversionError(sprintf(ERROR_MESSAGES.SPECIAL_PSEUDO_CLASS_SELECTOR_NOT_SUPPORTED, name));
}
}
}
/**
* Pre-scans a complex selector's child selectors
* for {@link AdgAttributeSelectors.MinLength}
* and {@link AdgAttributeSelectors.MaxLength} attribute selectors.
*
* Resolves duplicates to the most *restrictive* value:
* - for multiple `[min-length]` selectors, the largest value is selected;
* - for multiple `[max-length]` selectors, the smallest value is selected.
*
* Logs a warning when duplicate length selectors are found.
*
* @param selectors Child selectors of a complex selector to scan.
*
* @returns Resolved length constraints,
* or `null` if no length selectors were found.
*/
static collectLengthConstraints(selectors) {
const minValues = [];
const maxValues = [];
for (let i = 0; i < selectors.length; i += 1) {
const selector = selectors[i];
if (selector.type !== 'AttributeSelector') {
continue;
}
const { value: name } = selector.name;
if (name !== AdgAttributeSelectors.MinLength
&& name !== AdgAttributeSelectors.MaxLength) {
continue;
}
if (!('value' in selector) || selector.value.value === EMPTY) {
continue;
}
const { value } = selector.value;
HtmlRuleConverter.assertValidLengthValue(name, value, ERROR_MESSAGES.SPECIAL_ATTRIBUTE_SELECTOR_VALUE_INT, ERROR_MESSAGES.SPECIAL_ATTRIBUTE_SELECTOR_VALUE_POSITIVE);
if (name === AdgAttributeSelectors.MinLength) {
minValues.push(Number(value));
}
else {
maxValues.push(Number(value));
}
}
if (minValues.length === 0 && maxValues.length === 0) {
return null;
}
let min = null;
let max = null;
if (minValues.length > 1) {
min = Math.max(...minValues);
// eslint-disable-next-line no-console
console.warn(`Multiple [min-length] selectors found among: [${minValues.join(', ')}]. Selected largest: ${min}.`);
}
else if (minValues.length === 1) {
[min] = minValues;
}
if (maxValues.length > 1) {
max = Math.min(...maxValues);
// eslint-disable-next-line no-console
console.warn(`Multiple [max-length] selectors found among: [${maxValues.join(', ')}]. Selected smallest: ${max}.`);
}
else if (maxValues.length === 1) {
[max] = maxValues;
}
return { min, max };
}
/**
* Converts a HTML filtering rule body by handling special simple selectors via callbacks.
* Special simple selectors are skipped in the converted selector list and should be handled from callee.
*
* @param body HTML filtering rule body to convert.
* @param parser HTML filtering rule body parser used for parsing raw value bodies.
* @param generator HTML filtering rule body generator used for generating raw value bodies.
* @param onSpecialAttributeSelector Callback invoked when a special attribute selector is found.
* @param onSpecialPseudoClassSelector Callback invoked when a special pseudo-class selector is found.
* @param shouldMergeLengthSelectors If true, `[min-length]` and `[max-length]` attribute
* selectors within the same complex selector are merged into a single `:contains()` pseudo-class.
* Defaults to `false`.
*
* @returns Converted selector list without special simple selectors.
*/
static convertBody(body, parser, generator, onSpecialAttributeSelector, onSpecialPseudoClassSelector, shouldMergeLengthSelectors = false) {
// Handle case when body is raw value string.
// If so, parse it first as we need to work with AST nodes.
let processedBody;
if (body.type === 'Value') {
processedBody = parser.parse(body.value, {
isLocIncluded: false,
parseHtmlFilteringRuleBodies: true,
});
}
else {
processedBody = body;
}
const { children: complexSelectors } = processedBody.selectorList;
// Selector list node must not be empty
HtmlRuleConverter.assertNotEmpty(complexSelectors, ERROR_MESSAGES.EMPTY_SELECTOR_LIST);
// Convert each complex selector
const convertedComplexSelectors = [];
for (let i = 0; i < complexSelectors.length; i += 1) {
const { children: selectors } = complexSelectors[i];
// Complex selector node must not be empty
HtmlRuleConverter.assertNotEmpty(selectors, ERROR_MESSAGES.EMPTY_COMPLEX_SELECTOR);
// Pre-scan for [min-length] / [max-length] constraints to merge them into one :contains()
const lengthConstraints = shouldMergeLengthSelectors
? HtmlRuleConverter.collectLengthConstraints(selectors)
: null;
let lengthContainsEmitted = false;
// Convert each selector
const convertedSelectors = [];
for (let j = 0; j < selectors.length; j += 1) {
const selector = selectors[j];
switch (selector.type) {
case 'SelectorCombinator': {
// Throw if selector combinator used incorrectly
if (
// If first selector in the complex selector (`> div`)
j === 0
// If the previous selector is also a combinator (`div > + span`)
|| j === selectors.length - 1
// If the last selector in the complex selector (`div +`)
|| (j > 0 && selectors[j - 1].type === 'SelectorCombinator')) {
throw new RuleConversionError(sprintf(ERROR_MESSAGES.INVALID_RULE, sprintf(ERROR_MESSAGES.INVALID_SELECTOR_COMBINATOR, selector.value)));
}
break;
}
case 'AttributeSelector': {
// Not a special attribute selector - clone as-is after the switch
if (!SUPPORTED_ADG_ATTRIBUTE_SELECTORS.has(selector.name.value)) {
break;
}
// Throw an error if value is missing
if (!('value' in selector) || selector.value.value === EMPTY) {
throw new RuleConversionError(sprintf(ERROR_MESSAGES.SPECIAL_ATTRIBUTE_SELECTOR_VALUE_REQUIRED, selector.name.value));
}
// Throw an error if operator is not '='
if (selector.operator.value !== EQUALS) {
throw new RuleConversionError(sprintf(ERROR_MESSAGES.SPECIAL_ATTRIBUTE_SELECTOR_OPERATOR_INVALID, selector.name.value, selector.operator.value));
}
// Throw an error if flag is specified
if (selector.flag) {
throw new RuleConversionError(sprintf(ERROR_MESSAGES.SPECIAL_ATTRIBUTE_SELECTOR_FLAG_NOT_SUPPORTED, selector.name.value));
}
const name = selector.name.value;
const { value } = selector.value;
// Merge [min-length] and [max-length] into a single :contains() (ADG→ADG)
if (lengthConstraints !== null
&& (name === AdgAttributeSelectors.MinLength
|| name === AdgAttributeSelectors.MaxLength)) {
if (!lengthContainsEmitted) {
// Invoke the callback once to trigger its side effects
// (e.g. the isConverted flag in convertToAdg), but discard
// the individual :contains() it returns — we emit the
// merged one instead.
onSpecialAttributeSelector(name, value);
convertedSelectors.push(HtmlRuleConverter.getPseudoClassSelectorNode(AdgPseudoClasses.Contains, RegExpUtils.getLengthRegexp(lengthConstraints.min, lengthConstraints.max)));
lengthContainsEmitted = true;
}
continue;
}
// Invoke callback and:
// - add returned simple selector if it's not boolean
// - skip adding if returned value is false
// - keep original simple selector if returned value is true
const result = onSpecialAttributeSelector(name, value);
if (typeof result !== 'boolean') {
convertedSelectors.push(result);
continue;
}
else if (result === false) {
continue;
}
break;
}
case 'PseudoClassSelector': {
// Not a special pseudo-class selector - clone as-is after the switch
if (!SUPPORTED_ADG_PSEUDO_CLASSES.has(selector.name.value)
&& !SUPPORTED_UBO_PSEUDO_CLASSES.has(selector.name.value)) {
break;
}
// Throw an error if argument is missing
if (!selector.argument || selector.argument.value === EMPTY) {
throw new RuleConversionError(sprintf(ERROR_MESSAGES.SPECIAL_PSEUDO_CLASS_SELECTOR_ARGUMENT_REQUIRED, selector.name.value));
}
const name = selector.name.value;
const argument = selector.argument.value;
// Invoke callback and:
// - add returned simple selector if it's not boolean
// - skip adding if returned value is false
// - keep original simple selector if returned value is true
const result = onSpecialPseudoClassSelector(name, argument);
if (typeof result !== 'boolean') {
convertedSelectors.push(result);
continue;
}
else if (result === false) {
continue;
}
break;
}
}
// Clone selector if previous conditions are not met
convertedSelectors.push(HtmlRuleConverter.cloneSelector(selector));
}
convertedComplexSelectors.push({
type: 'ComplexSelector',
children: convertedSelectors,
});
}
let convertedBody = {
type: 'HtmlFilteringRuleBody',
selectorList: {
type: 'SelectorList',
children: convertedComplexSelectors,
},
};
// Convert back to Value if the original body was Value
if (body.type === 'Value') {
convertedBody = {
type: 'Value',
value: generator.generate(convertedBody),
};
}
return convertedBody;
}
/**
* Clones a simple selector or selector combinator node.
*
* @param selector Simple selector or selector combinator node to clone.
*
* @returns Cloned simple selector or selector combinator node.
*/
static cloneSelector(selector) {
const { type } = selector;
switch (type) {
case 'TypeSelector':
case 'IdSelector':
case 'ClassSelector':
return {
type: selector.type,
value: selector.value,
};
case 'SelectorCombinator':
return {
type: selector.type,
value: selector.value,
};
case 'AttributeSelector': {
const attributeSelectorClone = {
type: selector.type,
name: {
type: selector.name.type,
value: selector.name.value,
},
};
if ('value' in selector && selector.value) {
attributeSelectorClone.operator = {
type: selector.operator.type,
value: selector.operator.value,
};
attributeSelectorClone.value = {
type: selector.value.type,
value: selector.value.value,
};
if (selector.flag) {
attributeSelectorClone.flag = {
type: selector.flag.type,
value: selector.flag.value,
};
}
}
return attributeSelectorClone;
}
case 'PseudoClassSelector': {
const pseudoClassSelectorClone = {
type: selector.type,
name: {
type: selector.name.type,
value: selector.name.value,
},
};
if (selector.argument) {
pseudoClassSelectorClone.argument = {
type: selector.argument.type,
value: selector.argument.value,
};
}
return pseudoClassSelectorClone;
}
default: {
throw new RuleConversionError(sprintf(ERROR_MESSAGES.INVALID_RULE, sprintf(ERROR_MESSAGES.UNKNOWN_SELECTOR_TYPE, type)));
}
}
}
/**
* Creates a CSS pseudo-class selector node.
*
* @param name The name of the pseudo-class selector.
* @param argument Optional argument of the pseudo-class selector.
*
* @returns CSS pseudo-class selector node.
*/
static getPseudoClassSelectorNode(name, argument) {
return {
type: 'PseudoClassSelector',
name: {
type: 'Value',
value: name,
},
argument: argument ? {
type: 'Value',
value: argument,
} : undefined,
};
}
/**
* Asserts that the given array is not empty.
*
* @param array Array to check.
* @param errorMessage Error message to use if the array is empty.
*
* @throws If the array is empty.
*/
static assertNotEmpty(array, errorMessage) {
if (array.length === 0) {
throw new RuleConversionError(sprintf(ERROR_MESSAGES.INVALID_RULE, errorMessage));
}
}
/**
* Asserts that the given special attribute / pseudo-class length value is valid.
*
* @param name Name of the attribute or pseudo-class.
* @param value Value to parse.
* @param notIntErrorMessage Error message when the value is not an integer.
* @param notPositiveErrorMessage Error message when the value is not positive.
*
* @throws If the value is not a valid number or not positive.
*/
static assertValidLengthValue(name, value, notIntErrorMessage, notPositiveErrorMessage) {
const parsed = Number(value);
if (Number.isNaN(parsed)) {
throw new RuleConversionError(sprintf(notIntErrorMessage, name, value));
}
if (parsed < 0) {
throw new RuleConversionError(sprintf(notPositiveErrorMessage, name, value));
}
}
}
export { ERROR_MESSAGES, HtmlRuleConverter };