UNPKG

@adguard/agtree

Version:
150 lines (147 loc) 7.36 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 { sprintf } from 'sprintf-js'; import { CLOSE_PARENTHESIS, OPEN_PARENTHESIS, ADG_SCRIPTLET_MASK, SPACE, ESCAPE_CHARACTER, COMMA, SINGLE_QUOTE, DOUBLE_QUOTE } from '../../../utils/constants.js'; import { StringUtils } from '../../../utils/string.js'; import { AdblockSyntaxError } from '../../../errors/adblock-syntax-error.js'; import { defaultParserOptions } from '../../options.js'; import { BaseParser } from '../../base-parser.js'; import { ValueParser } from '../../misc/value-parser.js'; import { isNull } from '../../../utils/type-guards.js'; /** * @file AdGuard scriptlet injection body parser */ /** * `AdgScriptletInjectionBodyParser` is responsible for parsing the body of an AdGuard-style scriptlet rule. * * Please note that the parser will parse any scriptlet rule if it is syntactically correct. * For example, it will parse this: * ```adblock * example.com#%#//scriptlet('scriptlet0', 'arg0') * ``` * * but it didn't check if the scriptlet `scriptlet0` actually supported by any adblocker. * * @see {@link https://kb.adguard.com/en/general/how-to-create-your-own-ad-filters#scriptlets} */ class AdgScriptletInjectionBodyParser extends BaseParser { /** * Error messages used by the parser. */ static ERROR_MESSAGES = { NO_SCRIPTLET_MASK: `Invalid ADG scriptlet call, no scriptlet call mask '${ADG_SCRIPTLET_MASK}' found`, NO_OPENING_PARENTHESIS: `Invalid ADG scriptlet call, no opening parentheses '${OPEN_PARENTHESIS}' found`, NO_CLOSING_PARENTHESIS: `Invalid ADG scriptlet call, no closing parentheses '${CLOSE_PARENTHESIS}' found`, WHITESPACE_AFTER_MASK: 'Invalid ADG scriptlet call, whitespace is not allowed after the scriptlet call mask', NO_INCONSISTENT_QUOTES: 'Invalid ADG scriptlet call, inconsistent quotes', NO_UNCLOSED_PARAMETER: 'Invalid ADG scriptlet call, unclosed parameter', EXPECTED_QUOTE: "Invalid ADG scriptlet call, expected quote, got '%s'", EXPECTED_COMMA: "Invalid ADG scriptlet call, expected comma, got '%s'", }; /** * Parses the body of an AdGuard-style scriptlet rule. * * @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 scriptlet call body * @throws If the body is syntactically incorrect * @example * ``` * //scriptlet('scriptlet0', 'arg0') * ``` */ static parse(raw, options = defaultParserOptions, baseOffset = 0) { let offset = 0; // Skip leading spaces offset = StringUtils.skipWS(raw, offset); // Scriptlet call should start with "//scriptlet" if (!raw.startsWith(ADG_SCRIPTLET_MASK, offset)) { throw new AdblockSyntaxError(this.ERROR_MESSAGES.NO_SCRIPTLET_MASK, baseOffset + offset, baseOffset + raw.length); } offset += ADG_SCRIPTLET_MASK.length; // Whitespace is not allowed after the mask if (raw[offset] === SPACE) { throw new AdblockSyntaxError(this.ERROR_MESSAGES.WHITESPACE_AFTER_MASK, baseOffset + offset, baseOffset + raw.length); } // Parameter list should be wrapped in parentheses if (raw[offset] !== OPEN_PARENTHESIS) { throw new AdblockSyntaxError(this.ERROR_MESSAGES.NO_OPENING_PARENTHESIS, baseOffset + offset, baseOffset + raw.length); } // Save the offset of the opening parentheses const openingParenthesesIndex = offset; // Skip whitespace from the end const closingParenthesesIndex = StringUtils.skipWSBack(raw, raw.length - 1); // Closing parentheses should be present if (raw[closingParenthesesIndex] !== CLOSE_PARENTHESIS || raw[closingParenthesesIndex - 1] === ESCAPE_CHARACTER) { throw new AdblockSyntaxError(this.ERROR_MESSAGES.NO_CLOSING_PARENTHESIS, baseOffset + offset, baseOffset + raw.length); } // Skip space, if any offset = StringUtils.skipWS(raw, offset + 1); const result = { type: 'ScriptletInjectionRuleBody', children: [], }; if (options.isLocIncluded) { result.start = baseOffset; result.end = baseOffset + raw.length; } // Special case: empty scriptlet call, like `//scriptlet()`, `//scriptlet( )` etc. if (StringUtils.skipWS(raw, openingParenthesesIndex + 1) === closingParenthesesIndex) { return result; } let detectedQuote = null; const parameterList = { type: 'ParameterList', children: [], }; if (options.isLocIncluded) { parameterList.start = baseOffset + openingParenthesesIndex + 1; parameterList.end = baseOffset + closingParenthesesIndex; } while (offset < closingParenthesesIndex) { // Skip whitespace offset = StringUtils.skipWS(raw, offset); // Expect comma if not first parameter if (parameterList.children.length > 0) { if (raw[offset] !== COMMA) { throw new AdblockSyntaxError(sprintf(AdgScriptletInjectionBodyParser.ERROR_MESSAGES.EXPECTED_COMMA, raw[offset]), baseOffset + offset, baseOffset + raw.length); } // Eat the comma offset += 1; // Skip whitespace offset = StringUtils.skipWS(raw, offset); } // Next character should be a quote if (raw[offset] === SINGLE_QUOTE || raw[offset] === DOUBLE_QUOTE) { if (isNull(detectedQuote)) { detectedQuote = raw[offset]; } else if (detectedQuote !== raw[offset]) { throw new AdblockSyntaxError(AdgScriptletInjectionBodyParser.ERROR_MESSAGES.NO_INCONSISTENT_QUOTES, baseOffset + offset, baseOffset + raw.length); } // Find next unescaped same quote const closingQuoteIndex = StringUtils.findNextUnescapedCharacter(raw, detectedQuote, offset + 1); if (closingQuoteIndex === -1) { throw new AdblockSyntaxError(AdgScriptletInjectionBodyParser.ERROR_MESSAGES.NO_UNCLOSED_PARAMETER, baseOffset + offset, baseOffset + raw.length); } // Save the parameter const parameter = ValueParser.parse(raw.slice(offset, closingQuoteIndex + 1), options, baseOffset + offset); parameterList.children.push(parameter); // Move after the closing quote offset = StringUtils.skipWS(raw, closingQuoteIndex + 1); } else { throw new AdblockSyntaxError(sprintf(AdgScriptletInjectionBodyParser.ERROR_MESSAGES.EXPECTED_QUOTE, raw[offset]), baseOffset + offset, baseOffset + raw.length); } } result.children.push(parameterList); return result; } } export { AdgScriptletInjectionBodyParser };