@adguard/agtree
Version:
Tool set for working with adblock filter lists
138 lines (135 loc) • 6.13 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 JSON5 from 'json5';
import { AdblockSyntax } from '../../utils/adblockers.js';
import { AGLINT_CONFIG_COMMENT_MARKER, AGLINT_COMMAND_PREFIX, COMMA } from '../../utils/constants.js';
import { CommentMarker, RuleCategory, CommentRuleType } from '../../nodes/index.js';
import { StringUtils } from '../../utils/string.js';
import { ParameterListParser } from '../misc/parameter-list-parser.js';
import { defaultParserOptions } from '../options.js';
import { BaseParser } from '../base-parser.js';
import { ValueParser } from '../misc/value-parser.js';
/**
* @file AGLint configuration comments. Inspired by ESLint inline configuration comments.
* @see {@link https://eslint.org/docs/latest/user-guide/configuring/rules#using-configuration-comments}
*/
/**
* `ConfigCommentParser` is responsible for parsing inline AGLint configuration rules.
* Generally, the idea is inspired by ESLint inline configuration comments.
*
* @see {@link https://eslint.org/docs/latest/user-guide/configuring/rules#using-configuration-comments}
*/
class ConfigCommentParser extends BaseParser {
/**
* Checks if the raw rule is an inline configuration comment rule.
*
* @param raw Raw rule
* @returns `true` if the rule is an inline configuration comment rule, otherwise `false`.
*/
static isConfigComment(raw) {
const trimmed = raw.trim();
if (trimmed[0] === CommentMarker.Regular || trimmed[0] === CommentMarker.Hashmark) {
// Skip comment marker and trim comment text (it is necessary because of "! something")
const text = raw.slice(1).trim();
// The code below is "not pretty", but it runs fast, which is necessary, since it will run on EVERY comment
// The essence of the indicator is that the control comment always starts with the "aglint" prefix
return ((text[0] === 'a' || text[0] === 'A')
&& (text[1] === 'g' || text[1] === 'G')
&& (text[2] === 'l' || text[2] === 'L')
&& (text[3] === 'i' || text[3] === 'I')
&& (text[4] === 'n' || text[4] === 'N')
&& (text[5] === 't' || text[5] === 'T'));
}
return false;
}
/**
* Parses a raw rule as an inline configuration 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
* Inline configuration comment AST or null (if the raw rule cannot be parsed as configuration comment)
*/
static parse(raw, options = defaultParserOptions, baseOffset = 0) {
if (!ConfigCommentParser.isConfigComment(raw)) {
return null;
}
let offset = 0;
// Skip leading whitespace (if any)
offset = StringUtils.skipWS(raw, offset);
// Get comment marker
const marker = ValueParser.parse(raw[offset], options, baseOffset + offset);
// Skip marker
offset += 1;
// Skip whitespace (if any)
offset = StringUtils.skipWS(raw, offset);
// Save the command start position
const commandStart = offset;
// Get comment text, for example: "aglint-disable-next-line"
offset = StringUtils.findNextWhitespaceCharacter(raw, offset);
const command = ValueParser.parse(raw.slice(commandStart, offset), options, baseOffset + commandStart);
// Skip whitespace after command
offset = StringUtils.skipWS(raw, offset);
// Get comment (if any)
const commentStart = raw.indexOf(AGLINT_CONFIG_COMMENT_MARKER, offset);
const commentEnd = commentStart !== -1 ? StringUtils.skipWSBack(raw) + 1 : -1;
let comment;
// Check if there is a comment
if (commentStart !== -1) {
comment = ValueParser.parse(raw.slice(commentStart, commentEnd), options, baseOffset + commentStart);
}
// Get parameter
const paramsStart = offset;
const paramsEnd = commentStart !== -1
? StringUtils.skipWSBack(raw, commentStart - 1) + 1
: StringUtils.skipWSBack(raw) + 1;
let params;
// `! aglint ...` config comment
if (command.value === AGLINT_COMMAND_PREFIX) {
params = {
type: 'ConfigNode',
// It is necessary to use JSON5.parse instead of JSON.parse because JSON5 allows unquoted keys.
// But don't forget to add { } to the beginning and end of the string,
// otherwise JSON5 will not be able to parse it.
// TODO: Better solution? ESLint uses "levn" package for parsing these comments.
value: JSON5.parse(`{${raw.slice(paramsStart, paramsEnd)}}`),
};
if (options.isLocIncluded) {
params.start = paramsStart;
params.end = paramsEnd;
}
// Throw error for empty config
if (Object.keys(params.value).length === 0) {
throw new Error('Empty AGLint config');
}
}
else if (paramsStart < paramsEnd) {
params = ParameterListParser.parse(raw.slice(paramsStart, paramsEnd), options, baseOffset + paramsStart, COMMA);
}
const result = {
type: CommentRuleType.ConfigCommentRule,
category: RuleCategory.Comment,
syntax: AdblockSyntax.Common,
marker,
command,
params,
comment,
};
if (options.includeRaws) {
result.raws = {
text: raw,
};
}
if (options.isLocIncluded) {
result.start = baseOffset;
result.end = baseOffset + raw.length;
}
return result;
}
}
export { ConfigCommentParser };