@adguard/agtree
Version:
Tool set for working with adblock filter lists
165 lines (162 loc) • 6.91 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 { getFormattedTokenName, TokenType } from '@adguard/css-tokenizer';
import { AdblockSyntaxError } from '../../../errors/adblock-syntax-error.js';
import { SPACE, TILDE, PLUS, GREATER_THAN, DOT, ASTERISK } from '../../../utils/constants.js';
import { BaseParser } from '../../base-parser.js';
import { CssTokenStream } from '../../css/css-token-stream.js';
import { defaultParserOptions } from '../../options.js';
import { AttributeSelectorHandler } from './handlers/attribute-selector-handler.js';
import { ClassSelectorHandler } from './handlers/class-selector-handler.js';
import { ComplexSelectorHandler } from './handlers/complex-selector-handler.js';
import { CompoundSelectorHandler } from './handlers/compound-selector-handler.js';
import { IdSelectorHandler } from './handlers/id-selector-handler.js';
import { PseudoClassSelectorHandler } from './handlers/pseudo-class-selector-handler.js';
import { TypeSelectorHandler } from './handlers/type-selector-handler.js';
/**
* Class responsible for parsing selector lists.
*
* Please note that the parser will parse any selector list if it is syntactically correct.
* For example, it will parse this:.
* ```adblock
* div[attr1="value1"] > h1[attr2="value2"], span[attr3="value3"]
* ```
*
* But it didn't check if the given attribute or pseudo-class is valid or not..
*
* @see {@link https://www.w3.org/TR/selectors-4/#selector-list}'
*/
class SelectorListParser extends BaseParser {
/**
* Common error messages used in the parser for unexpected tokens.
*/
static UNEXPECTED_TOKEN_WITH_VALUE_ERROR = "Unexpected token '%s' with value '%s'";
/**
* Parses a selector list.
*
* @param raw Raw input to parse.
* @param options Global parser options.
* @param baseOffset Starting offset of the input. Node locations are calculated relative to this offset.
*
* @returns Node of the parsed selector list.
*
* @throws If the selector list is syntactically incorrect.
*
* @example
* ```
* div[attr1="value1"] > h1[attr2="value2"], span[attr3="value3"]
* ```
*/
static parse(raw, options = defaultParserOptions, baseOffset = 0) {
// Construct the stream
const stream = new CssTokenStream(raw, baseOffset);
// Skip whitespaces before first token
stream.skipWhitespace();
// Construct selector list parser context
const context = {
raw,
options,
baseOffset,
stream,
// Get first token
token: stream.getOrFail(),
// Construct selector list node
result: {
type: 'SelectorList',
children: [],
},
// Construct first complex selector node
complexSelector: {
type: 'ComplexSelector',
children: [],
},
// Track if type selector set in the current compound selector
isTypeSelectorSet: false,
};
// Include locations for (if needed):
// - start and end for the selector list node
// - start for the first complex selector node
if (options.isLocIncluded) {
context.result.start = baseOffset;
context.result.end = baseOffset + raw.length;
context.complexSelector.start = baseOffset + context.token.start;
}
// Traverse the stream
while (!stream.isEof()) {
// Get next token
context.token = stream.getOrFail();
switch (context.token.type) {
// Tag selector
case TokenType.Ident: {
TypeSelectorHandler.handle(context);
break;
}
// ID selector
case TokenType.Hash: {
IdSelectorHandler.handle(context);
break;
}
// Attribute selector
case TokenType.OpenSquareBracket: {
AttributeSelectorHandler.handle(context);
break;
}
// Pseudo-class selector
case TokenType.Colon: {
PseudoClassSelectorHandler.handle(context);
break;
}
// Universal type selector ('*'), Class selector ('.'), Combinators ('>', '+', '~')
case TokenType.Delim: {
// Get delimiter value
const delimiter = stream.fragment();
switch (delimiter) {
// Universal type selector ('*')
case ASTERISK: {
TypeSelectorHandler.handle(context);
break;
}
// Class selector ('.)
case DOT: {
ClassSelectorHandler.handle(context);
break;
}
// Combinators ('>', '+', '~')
case GREATER_THAN:
case PLUS:
case TILDE: {
CompoundSelectorHandler.handle(context, delimiter);
break;
}
default: {
throw new AdblockSyntaxError(sprintf(SelectorListParser.UNEXPECTED_TOKEN_WITH_VALUE_ERROR, getFormattedTokenName(context.token.type), delimiter), baseOffset + context.token.start, baseOffset + context.token.end);
}
}
break;
}
// End of current compound selector (whitespace combinator - descendant)
case TokenType.Whitespace: {
CompoundSelectorHandler.handle(context, SPACE);
break;
}
// End of current complex selector
case TokenType.Comma: {
ComplexSelectorHandler.handle(context, false);
break;
}
default: {
throw new AdblockSyntaxError(sprintf(SelectorListParser.UNEXPECTED_TOKEN_WITH_VALUE_ERROR, getFormattedTokenName(context.token.type), stream.fragment()), baseOffset + context.token.start, baseOffset + context.token.end);
}
}
}
// Finish last complex selector
ComplexSelectorHandler.handle(context);
return context.result;
}
}
export { SelectorListParser };