@adguard/agtree
Version:
Tool set for working with adblock filter lists
118 lines (115 loc) • 4.87 kB
JavaScript
/*
* 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 { HINT_MARKER, OPEN_PARENTHESIS, BACKSLASH, CLOSE_PARENTHESIS, HINT_MARKER_LEN } from '../../utils/constants.js';
import { StringUtils } from '../../utils/string.js';
import { RuleCategory, CommentRuleType } from '../../nodes/index.js';
import { HintParser } from './hint-parser.js';
import { AdblockSyntax } from '../../utils/adblockers.js';
import { AdblockSyntaxError } from '../../errors/adblock-syntax-error.js';
import { defaultParserOptions } from '../options.js';
import { BaseParser } from '../base-parser.js';
/**
* `HintRuleParser` is responsible for parsing AdGuard hint rules.
*
* @example
* The following hint rule
* ```adblock
* !+ NOT_OPTIMIZED PLATFORM(windows)
* ```
* contains two hints: `NOT_OPTIMIZED` and `PLATFORM`.
* @see {@link https://kb.adguard.com/en/general/how-to-create-your-own-ad-filters#hints}
*/
class HintCommentParser extends BaseParser {
/**
* Checks if the raw rule is a hint rule.
*
* @param raw Raw rule
* @returns `true` if the rule is a hint rule, `false` otherwise
*/
static isHintRule(raw) {
return raw.trim().startsWith(HINT_MARKER);
}
/**
* Parses a raw rule as a hint comment.
*
* @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 AST or null (if the raw rule cannot be parsed as a hint comment)
* @throws If the input matches the HINT pattern but syntactically invalid
* @see {@link https://kb.adguard.com/en/general/how-to-create-your-own-ad-filters#hints-1}
*/
static parse(raw, options = defaultParserOptions, baseOffset = 0) {
// Ignore non-hint rules
if (!HintCommentParser.isHintRule(raw)) {
return null;
}
let offset = 0;
// Skip whitespace characters before the rule
offset = StringUtils.skipWS(raw);
// Skip hint marker
offset += HINT_MARKER_LEN;
const hints = [];
// Collect hints. Each hint is a string, optionally followed by a parameter list,
// enclosed in parentheses. One rule can contain multiple hints.
while (offset < raw.length) {
// Split rule into raw hints (e.g. 'HINT_NAME' or 'HINT_NAME(PARAMS)')
// Hints are separated by whitespace characters, but we should ignore
// whitespace characters inside the parameter list
// Ignore whitespace characters before the hint
offset = StringUtils.skipWS(raw, offset);
// Save the start index of the hint
const hintStartIndex = offset;
// Find the end of the hint
let hintEndIndex = offset;
let balance = 0;
while (hintEndIndex < raw.length) {
if (raw[hintEndIndex] === OPEN_PARENTHESIS && raw[hintEndIndex - 1] !== BACKSLASH) {
balance += 1;
// Throw error for nesting
if (balance > 1) {
throw new AdblockSyntaxError('Invalid hint: nested parentheses are not allowed', baseOffset + hintStartIndex, baseOffset + hintEndIndex);
}
}
else if (raw[hintEndIndex] === CLOSE_PARENTHESIS && raw[hintEndIndex - 1] !== BACKSLASH) {
balance -= 1;
}
else if (StringUtils.isWhitespace(raw[hintEndIndex]) && balance === 0) {
break;
}
hintEndIndex += 1;
}
offset = hintEndIndex;
// Skip whitespace characters after the hint
offset = StringUtils.skipWS(raw, offset);
// Parse the hint
const hint = HintParser.parse(raw.slice(hintStartIndex, hintEndIndex), options, baseOffset + hintStartIndex);
hints.push(hint);
}
// Throw error if no hints were found
if (hints.length === 0) {
throw new AdblockSyntaxError('Empty hint rule', baseOffset, baseOffset + offset);
}
const result = {
type: CommentRuleType.HintCommentRule,
category: RuleCategory.Comment,
syntax: AdblockSyntax.Adg,
children: hints,
};
if (options.includeRaws) {
result.raws = {
text: raw,
};
}
if (options.isLocIncluded) {
result.start = baseOffset;
result.end = baseOffset + offset;
}
return result;
}
}
export { HintCommentParser };