UNPKG

@adguard/agtree

Version:
131 lines (128 loc) 6.24 kB
/* * 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 { EMPTY } from '../../../../utils/constants.js'; import { ValueParser } from '../../../misc/value-parser.js'; /** * Handles pseudo-class selector parsing in selector list. */ class PseudoClassSelectorHandler { /** * Handles pseudo-class selector parsing by creating a pseudo-class selector node * and appending it to the current complex selector node. * * @param context Selector list parser context. * * @throws If the pseudo-class selector is syntactically incorrect. */ static handle(context) { const { raw, options, baseOffset, stream, complexSelector, } = context; // Get colon token let token = stream.getOrFail(); // Save pseudo-class selector node start position const { start } = token; // Advance colon token stream.advance(); // Get pseudo-class selector name token token = stream.getOrFail(); // It should be a function or identifier const isFunction = token.type === TokenType.Function; if (!isFunction && token.type !== TokenType.Ident) { throw new AdblockSyntaxError(sprintf( // eslint-disable-next-line max-len `Expected '${getFormattedTokenName(TokenType.Ident)}' or '${getFormattedTokenName(TokenType.Function)}' as pseudo-class selector name, but got '%s' with value '%s'`, getFormattedTokenName(token.type), stream.fragment()), baseOffset + token.start, baseOffset + token.end); } // Extract pseudo-class selector name raw value let nameRaw; if (!isFunction) { nameRaw = raw.slice(token.start, token.end); } else { nameRaw = raw.slice(token.start, token.end - 1); // Exclude '(' } // Construct pseudo-class selector node const result = { type: 'PseudoClassSelector', name: ValueParser.parse(nameRaw, options, baseOffset + token.start), }; // Include pseudo-class selector node start location if needed if (options.isLocIncluded) { result.start = baseOffset + start; } // If it's a function, parse its argument if (isFunction) { // Save the function token's balance level for finding the matching close parenthesis const functionBalance = stream.getBalance(); // Advance pseudo-class selector name token stream.advance(); // Get pseudo-class selector argument token token = stream.getOrFail(); // Construct empty pseudo-class selector argument node by default result.argument = { type: 'Value', value: EMPTY, }; // Include pseudo-class selector argument node location if needed if (options.isLocIncluded) { result.argument.start = baseOffset + token.start; result.argument.end = baseOffset + token.start; } // Skip whitespaces after opening parenthesis stream.skipWhitespace(); // Get pseudo-class selector argument or closing parenthesis token token = stream.getOrFail(); // Check if there is any argument part if (token.type !== TokenType.CloseParenthesis) { // Use the function token's balance level to find the matching closing parenthesis const balance = functionBalance; // Skip leading whitespace stream.skipWhitespace(); // Get argument token token = stream.getOrFail(); // Save pseudo-class selector argument start position const argumentStart = token.start; // Track last non-whitespace token to handle trailing whitespace let lastNonWsToken; // Skip to the closing parenthesis at the matching balance level while (stream.get()?.balance !== balance - 1) { const currentToken = stream.get(); if (currentToken && currentToken.type !== TokenType.Whitespace) { lastNonWsToken = currentToken; } stream.advance(); } // Get closing parenthesis token token = stream.getOrFail(); // Save pseudo-class selector argument end position (after last non-whitespace token) const argumentEnd = lastNonWsToken ? lastNonWsToken.end : token.start; // Extract pseudo-class selector argument raw value (trimmed) // TODO: Consider parsing inner selectors (like :not(.class)) const argumentRaw = raw.slice(argumentStart, argumentEnd); // Specify pseudo-class selector argument node value result.argument.value = argumentRaw; // Include pseudo-class selector argument node location if needed if (options.isLocIncluded) { result.argument.start = baseOffset + argumentStart; result.argument.end = baseOffset + argumentStart + argumentRaw.length; } } // Expect close parenthesis token stream.expect(TokenType.CloseParenthesis); } // Include pseudo-class selector end location if needed if (options.isLocIncluded) { result.end = baseOffset + token.end; } // Append pseudo-class selector node to the current complex selector node complexSelector.children.push(result); // Advance pseudo-class selector name token (if ident) or closing parenthesis token (if function) stream.advance(); } } export { PseudoClassSelectorHandler };