@adguard/agtree
Version:
Tool set for working with adblock filter lists
150 lines (147 loc) • 7.68 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 { StringUtils } from '../../utils/string.js';
import { COMMA, ESCAPE_CHARACTER } from '../../utils/constants.js';
import { defaultParserOptions } from '../options.js';
import { ValueParser } from './value-parser.js';
import { AdblockSyntaxError } from '../../errors/adblock-syntax-error.js';
import { QUOTE_SET } from '../../utils/quotes.js';
import { ParameterListParser } from './parameter-list-parser.js';
/**
* Parser for uBO-specific parameter lists.
*/
class UboParameterListParser extends ParameterListParser {
/**
* Parses an "uBO-specific parameter list".
*
* @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 requireQuotes Whether to require quotes around the parameter values (default: false).
* @param supportedQuotes Set of accepted quotes (default: {@link QUOTE_SET}).
* @returns Parameter list node.
*
* @note Based on {@link https://github.com/gorhill/uBlock/blob/f9ab4b75041815e6e5690d80851189ae3dc660d0/src/js/static-filtering-parser.js#L607-L699} to provide consistency.
*/
static parse(raw, options = defaultParserOptions, baseOffset = 0, separator = COMMA, requireQuotes = false, supportedQuotes = QUOTE_SET) {
// Prepare the parameter list node
const params = {
type: 'ParameterList',
children: [],
};
const { length } = raw;
if (options.isLocIncluded) {
params.start = baseOffset;
params.end = baseOffset + length;
}
let offset = 0;
// TODO: Eliminate the need for extraNull
let extraNull = false;
while (offset < length) {
offset = StringUtils.skipWS(raw, offset);
const paramStart = offset;
let paramEnd = offset;
if (supportedQuotes.has(raw[offset])) {
// Find the closing quote
const possibleClosingQuoteIndex = StringUtils.findNextUnescapedCharacter(raw, raw[offset], offset + 1);
if (possibleClosingQuoteIndex !== -1) {
// Next non-whitespace character after the closing quote should be the separator
const nextSeparatorIndex = StringUtils.skipWS(raw, possibleClosingQuoteIndex + 1);
if (nextSeparatorIndex === length) {
// If the separator is not found, the param end is the end of the string
paramEnd = StringUtils.skipWSBack(raw, length - 1) + 1;
offset = length;
}
else if (raw[nextSeparatorIndex] === separator) {
// If the quote is followed by a separator, we can use it as a closing quote
paramEnd = possibleClosingQuoteIndex + 1;
offset = nextSeparatorIndex + 1;
}
else {
if (requireQuotes) {
throw new AdblockSyntaxError(`Expected separator, got: '${raw[nextSeparatorIndex]}'`, baseOffset + nextSeparatorIndex, baseOffset + length);
}
/**
* At that point found `possibleClosingQuoteIndex` is wrong
* | is `offset`
* ~ is `possibleClosingQuoteIndex`
* ^ is `nextSeparatorIndex`
*
* Example 1: "abc, ').cba='1'"
* | ~^
* Example 2: "abc, ').cba, '1'"
* | ~^
* Example 3: "abc, ').cba='1', cba"
* | ~^
*
* Search for separator before `possibleClosingQuoteIndex`
*/
const separatorIndexBeforeQuote = StringUtils.findNextUnescapedCharacterBackwards(raw, separator, possibleClosingQuoteIndex, ESCAPE_CHARACTER, offset + 1);
if (separatorIndexBeforeQuote !== -1) {
// Found separator before (Example 2)
paramEnd = StringUtils.skipWSBack(raw, separatorIndexBeforeQuote - 1) + 1;
offset = separatorIndexBeforeQuote + 1;
}
else {
// Didn't found separator before, search after
const separatorIndexAfterQuote = StringUtils.findNextUnescapedCharacter(raw, separator, possibleClosingQuoteIndex);
if (separatorIndexAfterQuote !== -1) {
// We found separator after (Example 3)
paramEnd = StringUtils.skipWSBack(raw, separatorIndexAfterQuote - 1) + 1;
offset = separatorIndexAfterQuote + 1;
}
else {
// If the separator is not found, the param end is the end of the string (Example 1)
paramEnd = StringUtils.skipWSBack(raw, length - 1) + 1;
offset = length;
}
}
}
}
else {
if (requireQuotes) {
throw new AdblockSyntaxError('Expected closing quote, got end of string', baseOffset + offset, baseOffset + length);
}
// If the closing quote is not found, the param end is the end of the string
paramEnd = StringUtils.skipWSBack(raw, length - 1) + 1;
offset = length;
}
}
else {
if (requireQuotes) {
throw new AdblockSyntaxError(`Expected quote, got: '${raw[offset]}'`, baseOffset + offset, baseOffset + length);
}
const nextSeparator = StringUtils.findNextUnescapedCharacter(raw, separator, offset);
if (nextSeparator === -1) {
// If the separator is not found, the param end is the end of the string
paramEnd = StringUtils.skipWSBack(raw, length - 1) + 1;
offset = length;
}
else {
// Param end should be the last non-whitespace character before the separator
paramEnd = StringUtils.skipWSBack(raw, nextSeparator - 1) + 1;
offset = nextSeparator + 1;
if (StringUtils.skipWS(raw, length - 1) === nextSeparator) {
extraNull = true;
}
}
}
if (paramStart < paramEnd) {
params.children.push(ValueParser.parse(raw.slice(paramStart, paramEnd), options, baseOffset + paramStart));
}
else {
params.children.push(null);
}
}
if (extraNull) {
params.children.push(null);
}
return params;
}
}
export { UboParameterListParser };