@adguard/agtree
Version:
Tool set for working with adblock filter lists
130 lines (127 loc) • 5 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 { AdblockSyntax } from '../../utils/adblockers.js';
import { COLON } from '../../utils/constants.js';
import { CommentMarker, RuleCategory, CommentRuleType } from '../../nodes/index.js';
import { defaultParserOptions } from '../options.js';
import { BaseParser } from '../base-parser.js';
import { ValueParser } from '../misc/value-parser.js';
/**
* @file Metadata comments
*/
/**
* Set of known metadata headers. This helps to quickly identify and validate
* metadata headers in the comments.
*/
const KNOWN_METADATA_HEADERS = new Set([
'Checksum',
'Description',
'Expires',
'Homepage',
'Last Modified',
'LastModified',
'Licence',
'License',
'Time Updated',
'TimeUpdated',
'Version',
'Title',
]);
/**
* `MetadataParser` is responsible for parsing metadata comments.
* Metadata comments are special comments that specify some properties of the list.
*
* @example
* For example, in the case of
* ```adblock
* ! Title: My List
* ```
* the name of the header is `Title`, and the value is `My List`, which means that
* the list title is `My List`, and it can be used in the adblocker UI.
* @see {@link https://help.eyeo.com/adblockplus/how-to-write-filters#special-comments}
*/
class MetadataCommentParser extends BaseParser {
/**
* Parses a raw rule as a metadata 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 Metadata comment AST or null (if the raw rule cannot be parsed as a metadata comment)
*/
static parse(raw, options = defaultParserOptions, baseOffset = 0) {
// Fast check to avoid unnecessary work
if (raw.indexOf(COLON) === -1) {
return null;
}
let offset = 0;
// Skip leading spaces before the comment marker
offset = StringUtils.skipWS(raw, offset);
// Check if the rule starts with a comment marker (first non-space sequence)
if (raw[offset] !== CommentMarker.Regular && raw[offset] !== CommentMarker.Hashmark) {
return null;
}
// Consume the comment marker
const marker = ValueParser.parse(raw[offset], options, baseOffset + offset);
offset += 1;
// Skip spaces
offset = StringUtils.skipWS(raw, offset);
// Save header start position
const headerStart = offset;
// Check if the comment text starts with a known header
const text = raw.slice(offset);
for (const knownHeader of KNOWN_METADATA_HEADERS) {
// Check if the comment text starts with the header (case-insensitive)
if (text.toLocaleLowerCase().startsWith(knownHeader.toLocaleLowerCase())) {
// Skip the header
offset += knownHeader.length;
// Save header
const header = ValueParser.parse(raw.slice(headerStart, offset), options, baseOffset + headerStart);
// Skip spaces after the header
offset = StringUtils.skipWS(raw, offset);
// Check if the rule contains a separator after the header
if (raw[offset] !== COLON) {
return null;
}
// Skip the separator
offset += 1;
// Skip spaces after the separator
offset = StringUtils.skipWS(raw, offset);
// Save the value start position
const valueStart = offset;
// Check if the rule contains a value
if (offset >= raw.length) {
return null;
}
const valueEnd = StringUtils.skipWSBack(raw, raw.length - 1) + 1;
// Save the value
const value = ValueParser.parse(raw.slice(valueStart, valueEnd), options, baseOffset + valueStart);
const result = {
type: CommentRuleType.MetadataCommentRule,
category: RuleCategory.Comment,
syntax: AdblockSyntax.Common,
marker,
header,
value,
};
if (options.includeRaws) {
result.raws = {
text: raw,
};
}
if (options.isLocIncluded) {
result.start = baseOffset;
result.end = baseOffset + raw.length;
}
return result;
}
}
return null;
}
}
export { MetadataCommentParser };