UNPKG

@adguard/agtree

Version:
130 lines (127 loc) 5.8 kB
/* * AGTree v3.2.2 (build date: Tue, 08 Jul 2025 13:39:47 GMT) * (c) 2025 Adguard Software Ltd. * Released under the MIT license * https://github.com/AdguardTeam/tsurlfilter/tree/master/packages/agtree#readme */ import { OPEN_PARENTHESIS, SPACE, UNDERSCORE, EMPTY, CLOSE_PARENTHESIS, COMMA } from '../../utils/constants.js'; import { StringUtils } from '../../utils/string.js'; import { AdblockSyntaxError } from '../../errors/adblock-syntax-error.js'; import { ParameterListParser } from '../misc/parameter-list-parser.js'; import { defaultParserOptions } from '../options.js'; import { BaseParser } from '../base-parser.js'; import { ValueParser } from '../misc/value-parser.js'; /* eslint-disable no-param-reassign */ /** * @file AdGuard Hints * @see {@link https://kb.adguard.com/en/general/how-to-create-your-own-ad-filters#hints} */ /** * `HintParser` is responsible for parsing AdGuard hints. * * @example * If the hint rule is * ```adblock * !+ NOT_OPTIMIZED PLATFORM(windows) * ``` * then the hints are `NOT_OPTIMIZED` and `PLATFORM(windows)`, and this * class is responsible for parsing them. The rule itself is parsed by * the `HintRuleParser`, which uses this class to parse single hints. */ class HintParser extends BaseParser { /** * Parses a raw rule as a hint. * * @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 Hint rule AST or null * @throws If the syntax is invalid */ static parse(raw, options = defaultParserOptions, baseOffset = 0) { let offset = 0; // Skip whitespace characters before the hint offset = StringUtils.skipWS(raw); // Hint should start with the hint name in every case // Save the start offset of the hint name const nameStartIndex = offset; // Parse the hint name for (; offset < raw.length; offset += 1) { const char = raw[offset]; // Abort consuming the hint name if we encounter a whitespace character // or an opening parenthesis, which means 'HIT_NAME(' case if (char === OPEN_PARENTHESIS || char === SPACE) { break; } // Hint name should only contain letters, digits, and underscores if (!StringUtils.isAlphaNumeric(char) && char !== UNDERSCORE) { throw new AdblockSyntaxError(`Invalid character "${char}" in hint name: "${char}"`, baseOffset + nameStartIndex, baseOffset + offset); } } // Save the end offset of the hint name const nameEndIndex = offset; // Save the hint name token const name = raw.slice(nameStartIndex, nameEndIndex); // Hint name cannot be empty if (name === EMPTY) { throw new AdblockSyntaxError('Empty hint name', baseOffset, baseOffset + nameEndIndex); } // Now we have two case: // 1. We have HINT_NAME and should return it // 2. We have HINT_NAME(PARAMS) and should continue parsing // Skip whitespace characters after the hint name offset = StringUtils.skipWS(raw, offset); // Throw error for 'HINT_NAME (' case if (offset > nameEndIndex && raw[offset] === OPEN_PARENTHESIS) { throw new AdblockSyntaxError('Unexpected whitespace(s) between hint name and opening parenthesis', baseOffset + nameEndIndex, baseOffset + offset); } // Create the hint name node (we can reuse it in the 'HINT_NAME' case, if needed) const nameNode = ValueParser.parse(name, options, baseOffset + nameStartIndex); // Just return the hint name if we have 'HINT_NAME' case (no params) if (raw[offset] !== OPEN_PARENTHESIS) { const result = { type: 'Hint', name: nameNode, }; if (options.isLocIncluded) { result.start = baseOffset; result.end = baseOffset + offset; } return result; } // Skip the opening parenthesis offset += 1; // Find closing parenthesis const closeParenthesisIndex = raw.lastIndexOf(CLOSE_PARENTHESIS); // Throw error if we don't have closing parenthesis if (closeParenthesisIndex === -1) { throw new AdblockSyntaxError(`Missing closing parenthesis for hint "${name}"`, baseOffset + nameStartIndex, baseOffset + raw.length); } // Save the start and end index of the params const paramsStartIndex = offset; const paramsEndIndex = closeParenthesisIndex; // Parse the params const params = ParameterListParser.parse(raw.slice(paramsStartIndex, paramsEndIndex), options, baseOffset + paramsStartIndex, COMMA); offset = closeParenthesisIndex + 1; // Skip whitespace characters after the closing parenthesis offset = StringUtils.skipWS(raw, offset); // Throw error if we don't reach the end of the input if (offset !== raw.length) { throw new AdblockSyntaxError( // eslint-disable-next-line max-len `Unexpected input after closing parenthesis for hint "${name}": "${raw.slice(closeParenthesisIndex + 1, offset + 1)}"`, baseOffset + closeParenthesisIndex + 1, baseOffset + offset + 1); } // Return the HINT_NAME(PARAMS) case AST const result = { type: 'Hint', name: nameNode, params, }; if (options.isLocIncluded) { result.start = baseOffset; result.end = baseOffset + offset; } return result; } } export { HintParser };