UNPKG

@adguard/agtree

Version:
156 lines (153 loc) 7.41 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 { AdblockSyntaxError } from '../../errors/adblock-syntax-error.js'; import { ListItemNodeType } from '../../nodes/index.js'; import { COMMA, NEGATION_MARKER, REGEX_MARKER, ESCAPE_CHARACTER, OPEN_SQUARE_BRACKET, CLOSE_SQUARE_BRACKET } from '../../utils/constants.js'; import { StringUtils } from '../../utils/string.js'; import { defaultParserOptions } from '../options.js'; /** * Prefixes for error messages which are used for parsing of value lists. */ const LIST_PARSE_ERROR_PREFIX = { EMPTY_ITEM: 'Empty value specified in the list', NO_MULTIPLE_NEGATION: 'Exception marker cannot be followed by another exception marker', NO_SEPARATOR_AFTER_NEGATION: 'Exception marker cannot be followed by a separator', NO_SEPARATOR_AT_THE_BEGINNING: 'Value list cannot start with a separator', NO_SEPARATOR_AT_THE_END: 'Value list cannot end with a separator', NO_WHITESPACE_AFTER_NEGATION: 'Exception marker cannot be followed by whitespace', }; /** * Parser for list items in modifiers. */ class ListItemsParser { /** * Parses a `raw` modifier value which may be represented as a list of items separated by `separator`. * Needed for $app, $denyallow, $domain, $method. * * @template T Type of the list items. * * @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. * @param separator Separator character (default: comma). * @param type Type of the list items (default: {@link ListItemNodeType.Domain}). * * @returns List of parsed items. * * @throws An {@link AdblockSyntaxError} if the list is syntactically invalid. * * @example * - parses an app list — `com.example.app|Example.exe` * - parses a domain list — `example.com,example.org,~example.org` or `example.com|~example.org` * - parses a method list — `~post|~put` */ static parse(raw, options = defaultParserOptions, baseOffset = 0, separator = COMMA, type = ListItemNodeType.Unknown) { // Function body here const rawListItems = []; let offset = 0; // Skip whitespace before the list offset = StringUtils.skipWS(raw, offset); // If the first character is a separator, then the list is invalid // and no need to continue parsing if (raw[offset] === separator) { throw new AdblockSyntaxError(LIST_PARSE_ERROR_PREFIX.NO_SEPARATOR_AT_THE_BEGINNING, baseOffset + offset, baseOffset + raw.length); } // If the last character is a separator, then the list item is invalid // and no need to continue parsing const realEndIndex = StringUtils.skipWSBack(raw); if (raw[realEndIndex] === separator) { throw new AdblockSyntaxError(LIST_PARSE_ERROR_PREFIX.NO_SEPARATOR_AT_THE_END, baseOffset + realEndIndex, baseOffset + realEndIndex + 1); } // Split list items by unescaped separators while (offset < raw.length) { // Skip whitespace before the list item offset = StringUtils.skipWS(raw, offset); const exception = raw[offset] === NEGATION_MARKER; const itemStart = exception ? offset + 1 : offset; let separatorStartIndex = -1; // item possibly a regex if (raw[itemStart] === REGEX_MARKER) { // try to find the next slash let i = itemStart + 1; let insideCharacterClass = false; let escaped = false; while (i < raw.length) { if (escaped) { escaped = false; } else if (raw[i] === ESCAPE_CHARACTER) { escaped = true; } else if (raw[i] === OPEN_SQUARE_BRACKET) { insideCharacterClass = true; } else if (raw[i] === CLOSE_SQUARE_BRACKET) { insideCharacterClass = false; } else if (!insideCharacterClass && raw[i] === REGEX_MARKER) { // check if slash followed by optional space followed by the separator const j = StringUtils.skipWS(raw, i + 1); if (raw[j] === separator) { separatorStartIndex = j; break; } } i += 1; } if (separatorStartIndex === -1) { separatorStartIndex = raw.length; } } else { separatorStartIndex = StringUtils.findNextUnescapedCharacter(raw, separator, itemStart); } const itemEnd = separatorStartIndex === -1 ? StringUtils.skipWSBack(raw) + 1 : StringUtils.skipWSBack(raw, separatorStartIndex - 1) + 1; // Skip the exception marker if (exception) { const item = raw[itemStart]; // Exception marker cannot be followed by another exception marker if (item === NEGATION_MARKER) { throw new AdblockSyntaxError(LIST_PARSE_ERROR_PREFIX.NO_MULTIPLE_NEGATION, baseOffset + itemStart, baseOffset + itemStart + 1); } // Exception marker cannot be followed by a separator if (item === separator) { throw new AdblockSyntaxError(LIST_PARSE_ERROR_PREFIX.NO_SEPARATOR_AFTER_NEGATION, baseOffset + itemStart, baseOffset + itemStart + 1); } // Exception marker cannot be followed by whitespace if (StringUtils.isWhitespace(item)) { throw new AdblockSyntaxError(LIST_PARSE_ERROR_PREFIX.NO_WHITESPACE_AFTER_NEGATION, baseOffset + itemStart, baseOffset + itemStart + 1); } } // List item can't be empty // Note we use '<=' instead of '===' because we have bidirectional trim // This is needed to handle cases like 'example.com, ,example.org' if (itemEnd <= itemStart) { throw new AdblockSyntaxError(LIST_PARSE_ERROR_PREFIX.EMPTY_ITEM, baseOffset + itemStart, baseOffset + raw.length); } const listItem = { type, value: raw.slice(itemStart, itemEnd), exception, }; if (options.isLocIncluded) { listItem.start = baseOffset + itemStart; listItem.end = baseOffset + itemEnd; } // Collect list item rawListItems.push(listItem); // Increment the offset to the next list item (or the end of the string) offset = separatorStartIndex === -1 ? raw.length : separatorStartIndex + 1; } return rawListItems; } } export { LIST_PARSE_ERROR_PREFIX, ListItemsParser };