@adguard/aglint
Version:
Universal adblock filter list linter.
166 lines (163 loc) • 6.61 kB
JavaScript
/*
* AGLint v3.0.0 (build date: Wed, 21 May 2025 13:24:14 GMT)
* (c) 2025 AdGuard Software Ltd.
* Released under the MIT license
* https://github.com/AdguardTeam/AGLint#readme
*/
import { walk, generate } from '@adguard/ecss-tree';
import { OPEN_PARENTHESIS, COLON, OPEN_SQUARE_BRACKET, SPACE, CLOSE_SQUARE_BRACKET, COMMA, HASHMARK, DOT, EMPTY, CLOSE_PARENTHESIS } from '../../common/constants.js';
import { CssTreeNodeType } from './css-tree-types.js';
/**
* Migrated from https://github.com/AdguardTeam/tsurlfilter/blob/2144959e92f94e4738274ce577c2fe26e3a0c08b/packages/agtree/src/utils/csstree.ts
*
* We implement our custom generate logic for some CSS nodes because CSSTree's default
* generator does not add spaces, just in some edge cases, but we want to show some formatted code.
*/
/**
* Selector generation based on CSSTree's AST. This is necessary because CSSTree
* only adds spaces in some edge cases.
*
* @param selectorNode CSS Tree AST
* @returns CSS selector as string
*/
const generateSelector = (selectorNode) => {
let result = EMPTY;
let inAttributeSelector = false;
let depth = 0;
let selectorListDepth = -1;
let prevNode = selectorNode;
walk(selectorNode, {
enter: (node) => {
depth += 1;
// Skip attribute selector or selector list children
if (inAttributeSelector || selectorListDepth > -1) {
return;
}
switch (node.type) {
// "Trivial" nodes
case CssTreeNodeType.TypeSelector:
result += node.name;
break;
case CssTreeNodeType.ClassSelector:
result += DOT;
result += node.name;
break;
case CssTreeNodeType.IdSelector:
result += HASHMARK;
result += node.name;
break;
case CssTreeNodeType.Identifier:
result += node.name;
break;
case CssTreeNodeType.Raw:
result += node.value;
break;
// "Advanced" nodes
case CssTreeNodeType.Nth:
// Default generation enough
result += generate(node);
break;
// For example :not([id], [name])
case CssTreeNodeType.SelectorList:
// eslint-disable-next-line no-case-declarations
const selectors = [];
node.children.forEach((selector) => {
if (selector.type === CssTreeNodeType.Selector) {
selectors.push(generateSelector(selector));
}
else if (selector.type === CssTreeNodeType.Raw) {
selectors.push(selector.value);
}
});
// Join selector lists
result += selectors.join(COMMA + SPACE);
// Skip nodes here
selectorListDepth = depth;
break;
case CssTreeNodeType.Combinator:
if (node.name === SPACE) {
result += node.name;
break;
}
// Prevent this case (unnecessary space): has( > .something)
if (prevNode.type !== CssTreeNodeType.Selector) {
result += SPACE;
}
result += node.name;
result += SPACE;
break;
case CssTreeNodeType.AttributeSelector:
result += OPEN_SQUARE_BRACKET;
// Identifier name
if (node.name) {
result += node.name.name;
}
// Matcher operator, eg =
if (node.matcher) {
result += node.matcher;
// Value can be String, Identifier or null
if (node.value !== null) {
// String node
if (node.value.type === CssTreeNodeType.String) {
result += generate(node.value);
}
else if (node.value.type === CssTreeNodeType.Identifier) {
// Identifier node
result += node.value.name;
}
}
}
// Flags
if (node.flags) {
// Space before flags
result += SPACE;
result += node.flags;
}
result += CLOSE_SQUARE_BRACKET;
inAttributeSelector = true;
break;
case CssTreeNodeType.PseudoElementSelector:
result += COLON;
result += COLON;
result += node.name;
if (node.children !== null) {
result += OPEN_PARENTHESIS;
}
break;
case CssTreeNodeType.PseudoClassSelector:
result += COLON;
result += node.name;
if (node.children !== null) {
result += OPEN_PARENTHESIS;
}
break;
}
prevNode = node;
},
leave: (node) => {
depth -= 1;
if (node.type === CssTreeNodeType.SelectorList && depth + 1 === selectorListDepth) {
selectorListDepth = -1;
}
if (selectorListDepth > -1) {
return;
}
if (node.type === CssTreeNodeType.AttributeSelector) {
inAttributeSelector = false;
}
if (inAttributeSelector) {
return;
}
switch (node.type) {
case CssTreeNodeType.PseudoElementSelector:
case CssTreeNodeType.PseudoClassSelector:
if (node.children !== null) {
result += CLOSE_PARENTHESIS;
}
break;
}
},
});
return result.trim();
};
export { generateSelector };