@adguard/agtree
Version:
Tool set for working with adblock filter lists
126 lines (123 loc) • 6.23 kB
JavaScript
/*
* AGTree v3.4.3 (build date: Thu, 11 Dec 2025 13:43:19 GMT)
* (c) 2025 Adguard Software Ltd.
* Released under the MIT license
* https://github.com/AdguardTeam/tsurlfilter/tree/master/packages/agtree#readme
*/
import { MODIFIERS_SEPARATOR } from '../../utils/constants.js';
import { StringUtils } from '../../utils/string.js';
import { BaseParser } from '../base-parser.js';
import { defaultParserOptions } from '../options.js';
import { ModifierParser } from './modifier-parser.js';
/* eslint-disable no-param-reassign */
/**
* `ModifierListParser` is responsible for parsing modifier lists. Please note that the name is not
* uniform, "modifiers" are also known as "options".
*
* @see {@link https://kb.adguard.com/en/general/how-to-create-your-own-ad-filters#basic-rules-modifiers}
* @see {@link https://kb.adguard.com/en/general/how-to-create-your-own-ad-filters#non-basic-rules-modifiers}
* @see {@link https://help.eyeo.com/adblockplus/how-to-write-filters#options}
*/
class ModifierListParser extends BaseParser {
/**
* Parses the cosmetic rule modifiers, eg. `third-party,domain=example.com|~example.org`.
*
* _Note:_ you should remove `$` separator before passing the raw modifiers to this function,
* or it will be parsed in the first modifier.
*
* @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 Parsed modifiers interface
*/
static parse(raw, options = defaultParserOptions, baseOffset = 0) {
const result = {
type: 'ModifierList',
children: [],
};
if (options.isLocIncluded) {
result.start = baseOffset;
result.end = baseOffset + raw.length;
}
let offset = StringUtils.skipWS(raw);
let separatorIndex = -1;
// Split modifiers by unescaped commas
while (offset < raw.length) {
// Skip whitespace before the modifier
offset = StringUtils.skipWS(raw, offset);
const modifierStart = offset;
// Check if this modifier has a regexp pattern
// Look for the `=` sign to find where the modifier value starts
let useSimpleSearch = false;
const equalsIndex = raw.indexOf('=', offset);
if (equalsIndex !== -1 && equalsIndex < raw.length - 1) {
const valueStart = equalsIndex + 1;
// Check if value starts with / (potential regex)
if (raw[valueStart] === '/' && raw[valueStart + 1] !== '/') {
// Look for a closing / for the regex pattern
// Search through the rest of the string for an unescaped /
let firstClosingSlashIndex = -1;
for (let i = valueStart + 1; i < raw.length; i += 1) {
if (raw[i] === '/' && raw[i - 1] !== '\\') {
firstClosingSlashIndex = i;
break;
}
}
if (firstClosingSlashIndex === -1) {
// No closing slash found anywhere - incomplete regex pattern
// Use simple search to allow it to work when it's the last modifier
useSimpleSearch = true;
}
else {
// Found a closing slash - check if there are MORE slashes after it
// If yes, this might be a complex pattern like replace=/pattern/replacement/flags
// and we should use simple search to avoid breaking it
let hasMoreSlashes = false;
for (let i = firstClosingSlashIndex + 1; i < raw.length; i += 1) {
if (raw[i] === '/' && raw[i - 1] !== '\\') {
hasMoreSlashes = true;
break;
}
}
// Use simple search if there are more slashes (complex pattern)
if (hasMoreSlashes) {
useSimpleSearch = true;
}
}
}
else {
// Value doesn't start with `/`, so it's not a regexp pattern.
// Use simple search to avoid treating slashes in values
// as regex markers, e.g. `redirect=googlesyndication.com/adsbygoogle.js`.
useSimpleSearch = true;
}
}
// Find the index of the first unescaped comma
let nextSeparator;
if (useSimpleSearch) {
// Use simple search for incomplete regex patterns
nextSeparator = StringUtils.findNextUnescapedCharacter(raw, MODIFIERS_SEPARATOR, offset);
}
else {
// Use regex-aware search to handle complete regex patterns
nextSeparator = StringUtils.findUnescapedNonStringNonRegexChar(raw, MODIFIERS_SEPARATOR, offset);
}
separatorIndex = nextSeparator;
const modifierEnd = separatorIndex === -1
? raw.length
: StringUtils.skipWSBack(raw, separatorIndex - 1) + 1;
// Parse the modifier
const modifier = ModifierParser.parse(raw.slice(modifierStart, modifierEnd), options, baseOffset + modifierStart);
result.children.push(modifier);
// Increment the offset to the next modifier (or the end of the string)
offset = separatorIndex === -1 ? raw.length : separatorIndex + 1;
}
// Check if there are any modifiers after the last separator
if (separatorIndex !== -1) {
const modifierStart = StringUtils.skipWS(raw, separatorIndex + 1);
result.children.push(ModifierParser.parse(raw.slice(modifierStart, raw.length), options, baseOffset + modifierStart));
}
return result;
}
}
export { ModifierListParser };