@adguard/agtree
Version:
Tool set for working with adblock filter lists
207 lines (204 loc) • 9.21 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 { TokenType, getFormattedTokenName } from '@adguard/css-tokenizer';
import { AdblockSyntaxError } from '../../../../errors/adblock-syntax-error.js';
import { TILDE, CARET, DOLLAR_SIGN, ASTERISK, PIPE, EQUALS } from '../../../../utils/constants.js';
import { QuoteUtils } from '../../../../utils/quotes.js';
import { ValueParser } from '../../../misc/value-parser.js';
/**
* Handles attribute selector parsing in selector list.
*/
class AttributeSelectorHandler {
/**
* Set of attribute equality prefixes.
*
* @see {@link AttributeSelectorOperatorValue}
*/
static ATTR_EQUALITY_PREFIXES = new Set([
// [attr~="value"]
TILDE,
// [attr^="value"]
CARET,
// [attr$="value"]
DOLLAR_SIGN,
// [attr*="value"]
ASTERISK,
// [attr|="value"]
PIPE,
]);
/**
* Set of valid flags for attribute selector values.
*
* @see {@link AttributeSelectorFlagValue}
*/
static ALLOWED_ATTRIBUTE_FLAGS = new Set([
'i',
's',
]);
/**
* Handles attribute selector parsing by creating an attribute selector node
* and appending it to the current complex selector node.
*
* @param context Selector list parser context.
*
* @throws If the attribute selector is syntactically incorrect.
*/
static handle(context) {
const { options, baseOffset, stream, complexSelector, } = context;
// Get open square bracket token
let token = stream.getOrFail();
// Save attribute selector node start position
const { start } = token;
// Advance open square bracket token
stream.advance();
// Skip whitespaces after open square bracket
stream.skipWhitespace();
// Expect next token to be an identifier (attribute selector name)
stream.expect(TokenType.Ident);
// Get attribute selector name token
token = stream.getOrFail();
// Extract attribute selector name raw value
const nameRaw = stream.fragment();
// Construct attribute selector node and attribute selector name node
const result = {
type: 'AttributeSelector',
name: ValueParser.parse(nameRaw, options, baseOffset + token.start),
};
// Include attribute selector node start location if needed
if (options.isLocIncluded) {
result.start = baseOffset + start;
}
// Advance attribute selector name token
stream.advance();
// Skip whitespaces after attribute selector name
stream.skipWhitespace();
// Get closing square bracket or equality sign/prefix token
token = stream.getOrFail();
// Check if there is an any value
if (token.type !== TokenType.CloseSquareBracket) {
// Expect next token to be a delimiter (equality sign/prefix)
stream.expect(TokenType.Delim);
// Extract operator raw value
let operatorRaw = stream.fragment();
// Check if it's prefix operator
if (AttributeSelectorHandler.ATTR_EQUALITY_PREFIXES.has(operatorRaw)) {
// Advance prefix operator token
stream.advance();
// Expect equal sign token
stream.expect(TokenType.Delim, { value: EQUALS });
// Append equal sign to prefix value
operatorRaw += EQUALS;
}
else if (operatorRaw !== EQUALS) {
// Throw error if it's not equal sign either
throw new AdblockSyntaxError(sprintf("Invalid attribute selector operator '%s'", operatorRaw), baseOffset + token.start, baseOffset + token.end);
}
// Save operator node start position
const operatorStart = token.start;
// Advance equal sign token
stream.advance();
// Skip whitespaces after equal sign
stream.skipWhitespace();
// Get attribute selector value token
token = stream.getOrFail();
// It should be a string or identifier
const isValueString = token.type === TokenType.String;
if (!isValueString && token.type !== TokenType.Ident) {
throw new AdblockSyntaxError(sprintf(
// eslint-disable-next-line max-len
`Expected '${getFormattedTokenName(TokenType.Ident)}' or '${getFormattedTokenName(TokenType.String)}' as attribute selector value, but got '%s' with value '%s'`, getFormattedTokenName(token.type), stream.fragment()), baseOffset + token.start, baseOffset + token.end);
}
// Save attribute selector value node start position
const valueStart = token.start + (isValueString ? 1 : 0);
// Save attribute selector value node end position
const valueEnd = token.end - (isValueString ? 1 : 0);
// Extract attribute selector value raw value
const valueRaw = stream.fragment();
// We should unescape respective quotes inside of the string value
const valueUnquotedAndUnescaped = QuoteUtils.removeQuotesAndUnescape(valueRaw);
// Construct attribute selector operator node
const operatorNode = {
type: 'Value',
value: operatorRaw,
};
// Construct attribute selector value node
const valueNode = {
type: 'Value',
value: valueUnquotedAndUnescaped,
};
// Set attribute selector operator and value nodes to result node
result.operator = operatorNode;
result.value = valueNode;
// Include attribute selector operator and value nodes locations if needed
if (options.isLocIncluded) {
operatorNode.start = baseOffset + operatorStart;
operatorNode.end = baseOffset + operatorStart + operatorRaw.length;
valueNode.start = baseOffset + valueStart;
valueNode.end = baseOffset + valueEnd;
}
// Advance attribute selector value token
stream.advance();
// Skip whitespaces after attribute selector value
stream.skipWhitespace();
// Get close square bracket or attribute selector value flag token
token = stream.getOrFail();
// Check if there is an any flag part
if (token.type !== TokenType.CloseSquareBracket) {
// Expect a identifier (attribute selector value flag)
stream.expect(TokenType.Ident);
// Extract attribute selector value flag raw value
const flagRaw = stream.fragment();
// Validate attribute selector value flag
if (!AttributeSelectorHandler.isValidFlag(flagRaw)) {
throw new AdblockSyntaxError(sprintf("Unexpected token '%s' with value '%s'", getFormattedTokenName(token.type), flagRaw), baseOffset + token.start, baseOffset + token.end);
}
// Save flag node start position
const flagStart = token.start;
// Construct attribute selector flag node
const flagNode = {
type: 'Value',
value: flagRaw,
};
// Set attribute selector flag node to result node
result.flag = flagNode;
// Include attribute selector flag node locations if needed
if (options.isLocIncluded) {
flagNode.start = baseOffset + flagStart;
flagNode.end = baseOffset + flagStart + flagRaw.length;
}
// Advance attribute selector value flag token
stream.advance();
// Skip whitespaces after attribute selector value flag
stream.skipWhitespace();
// Get close square bracket token
token = stream.getOrFail();
}
}
// Expect close square bracket token
stream.expect(TokenType.CloseSquareBracket);
// Include attribute selector node end location if needed
if (options.isLocIncluded) {
result.end = baseOffset + token.end;
}
// Append attribute selector node to the current complex selector node
complexSelector.children.push(result);
// Advance close square bracket token
stream.advance();
}
/**
* Validates attribute selector flag.
*
* @param flag Attribute selector flag.
*
* @returns `true` if the flag is valid, otherwise `false`.
*/
static isValidFlag(flag) {
return AttributeSelectorHandler.ALLOWED_ATTRIBUTE_FLAGS.has(flag);
}
}
export { AttributeSelectorHandler };