postcss-query-ast
Version:
Query PostCSS AST with CSS selectors.
538 lines (508 loc) • 18.9 kB
JavaScript
import * as postcss from 'postcss';
import parser from 'postcss-selector-parser';
import Container from 'postcss/lib/container';
const NTH_CHILD_QUERY_REGEX = /^(-)?(\d*)?n$/; // eslint-disable-line unicorn/no-unsafe-regex
/**
* @typedef {import('postcss-selector-parser').Root} selectorParser.Root
* @typedef {import('postcss-selector-parser').Node} selectorParser.Node
*/
/**
* @param {selectorParser.Root} selectors
*/
function normalizeTagSelector(selectors) {
selectors.walk(( /** @type {selectorParser.Node} */selector, /** @type {number} */index) => {
if (['selector', 'tag', 'universal'].includes(selector.type)) {
return;
}
let referenceSelector = null;
if (index === 0 && selector.type !== 'combinator') {
referenceSelector = selector;
}
if (index !== 0 && selector.type === 'combinator') {
referenceSelector = selector.next();
}
if (referenceSelector !== null && !['tag', 'universal'].includes(referenceSelector.type)) {
var _selector$parent;
selector === null || selector === void 0 || (_selector$parent = selector.parent) === null || _selector$parent === void 0 ? void 0 : _selector$parent.insertBefore(referenceSelector, parser.universal());
}
});
}
/**
* @param {selectorParser.Root} selectors
*/
function normalizeNthChildSelector(selectors) {
selectors.walkTags(number => {
var _number$parent;
if (((_number$parent = number.parent) === null || _number$parent === void 0 || (_number$parent = _number$parent.parent) === null || _number$parent === void 0 ? void 0 : _number$parent.type) !== 'pseudo') {
return;
}
const position = Number(number.value);
const previousSelector = number.prev();
const nextSelector = number.next();
if (number.type === 'tag' && Number.isInteger(position) && position >= 0 && !previousSelector && !nextSelector) {
number.parent.insertBefore(number, parser.tag({
value: '0n'
}));
number.parent.insertBefore(number, parser.combinator({
value: '+'
}));
number.parent.insertBefore(number, parser.tag({
value: String(position)
}));
number.remove();
}
if (number.type === 'tag' && number.value === 'odd') {
number.parent.insertBefore(number, parser.tag({
value: '2n'
}));
number.parent.insertBefore(number, parser.combinator({
value: '+'
}));
number.parent.insertBefore(number, parser.tag({
value: '1'
}));
number.remove();
}
if (number.type === 'tag' && number.value === 'even') {
number.parent.insertBefore(number, parser.tag({
value: '2n'
}));
number.parent.insertBefore(number, parser.combinator({
value: '+'
}));
number.parent.insertBefore(number, parser.tag({
value: '2'
}));
number.remove();
}
if (NTH_CHILD_QUERY_REGEX.test(number.value)) {
var _number$value$match;
const [, negative = '', regexNumber = 1] = (_number$value$match = number.value.match(NTH_CHILD_QUERY_REGEX)) !== null && _number$value$match !== void 0 ? _number$value$match : [];
const isAlone = !nextSelector;
const isInvalidCombinator = nextSelector && nextSelector.type !== 'combinator' && nextSelector.value !== '+';
if (isInvalidCombinator) {
number.parent.insertBefore(number, parser.tag({
value: '-n'
}));
number.parent.insertBefore(number, parser.combinator({
value: '+'
}));
number.parent.insertBefore(number, parser.tag({
value: String(0)
}));
} else {
const value = isAlone ? `${regexNumber}n` : `${negative}${regexNumber}n`;
number.parent.insertBefore(number, parser.tag({
value
}));
if (isAlone && negative === '') {
number.parent.insertBefore(number, parser.combinator({
value: '+'
}));
number.parent.insertBefore(number, parser.tag({
value: String(regexNumber)
}));
}
}
number.remove();
}
});
}
/**
* @param {string} query
*/
var getSelectorAst = (query => {
return parser(selectors => {
normalizeTagSelector(selectors);
normalizeNthChildSelector(selectors);
}).ast(query);
});
/**
* @typedef {import('postcss').ChildNode} postcss.ChildNode
*/
class PassthroughContainer extends Container {
constructor() {
super();
this.type = 'passthrough-container';
/** @type {postcss.ChildNode[]} */
this.nodes = [];
}
/**
* @param {postcss.ChildNode[]} nodes
*/
normalize(nodes) {
return [nodes];
}
}
/**
* @typedef {import('postcss').ChildNode} postcss.ChildNode
* @typedef {import('postcss-selector-parser').Combinator} selectorParser.Combinator
* @typedef {import('postcss-selector-parser').Node} selectorParser.Node
*/
/**
* @param {postcss.ChildNode} node
* @param {selectorParser.Node} selector
*/
function isValidNode(node, selector) {
return selector && (selector.type === 'tag' && selector.value === node.type || selector.type === 'universal');
}
/**
* @param {postcss.ChildNode} node
* @param {selectorParser.Combinator} selector
*/
var getCombinator = ((node, selector) => {
var _node$parent;
const container = new PassthroughContainer();
const result = [];
const nodeIndex = (_node$parent = node.parent) === null || _node$parent === void 0 ? void 0 : _node$parent.index(node);
const nextNode = node.next();
const nextSelector = selector.next();
const isNodeContainer = node instanceof Container;
if (selector.value === ' ' && isNodeContainer) {
node.walk(resolvedNode => {
container.append(resolvedNode);
});
result.push(container);
}
if (selector.value === '+') {
if (typeof nextNode !== 'undefined' && isValidNode(nextNode, nextSelector)) {
container.append(nextNode);
}
result.push(container);
}
if (selector.value === '~') {
var _node$parent2;
(_node$parent2 = node.parent) === null || _node$parent2 === void 0 ? void 0 : _node$parent2.each((resolvedNode, index) => {
if (typeof nodeIndex !== 'undefined' && index > nodeIndex && isValidNode(resolvedNode, nextSelector)) {
container.append(resolvedNode);
}
});
result.push(container);
}
if (selector.value === '>' && isNodeContainer) {
node.each(resolvedNode => {
if (nextSelector && isValidNode(resolvedNode, nextSelector)) {
container.append(resolvedNode);
}
});
result.push(container);
}
return result;
});
/**
* @typedef {import('postcss').ChildNode} postcss.ChildNode
* @typedef {import('postcss').Root} postcss.Root
* @typedef {import('postcss-selector-parser').Tag} selectorParser.Tag
* @typedef {import('postcss-selector-parser').Universal} selectorParser.Universal
* @typedef {import('../passthrough-container').default} PassthroughContainer
*/
/**
* @param {postcss.ChildNode[]} result
*/
function addNodeToResult(result) {
/**
* @param {postcss.ChildNode} resolvedNode
*/
return resolvedNode => {
result.push(resolvedNode);
};
}
/**
* @param {postcss.Root|PassthroughContainer} node
* @param {selectorParser.Tag|selectorParser.Universal} selector
*/
var getTag = ((node, selector) => {
const previousSelector = selector.prev();
/** @type {postcss.ChildNode[]} */
const result = [];
const isPreviousSelectorCombinator = previousSelector && previousSelector.type === 'combinator' && ['>', '+', '~'].includes(previousSelector.value);
const callback = addNodeToResult(result);
if (selector.type === 'universal') {
if (isPreviousSelectorCombinator) {
node.each(callback);
} else {
node.walk(callback);
}
}
if (selector.type === 'tag') {
if (isPreviousSelectorCombinator) {
node.each(resolvedNode => {
if (selector.value === resolvedNode.type) {
callback(resolvedNode);
}
});
} else {
switch (selector.value) {
case 'decl':
node.walkDecls(callback);
break;
case 'atrule':
node.walkAtRules(callback);
break;
case 'rule':
node.walkRules(callback);
break;
case 'comment':
node.walkComments(callback);
break;
}
}
}
return result;
});
/**
* @typedef {import('postcss').ChildNode} postcss.ChildNode
* @typedef {import('postcss-selector-parser').Attribute} selectorParser.Attribute
* @typedef {import('postcss-selector-parser').Node} selectorParser.Node
*/
const REGEX_REGEX = /^\/(.+)\/([gimuy]+)?$/; // eslint-disable-line unicorn/no-unsafe-regex
/**
* @param {string} regex
* @param {string} flags
* @param {postcss.ChildNode} node
* @param {selectorParser.Attribute} selector
*/
function isRegexMatched(regex, flags, node, selector) {
/** @type {string} */
// @ts-ignore
const composedAttribute = node[selector.attribute];
return new RegExp(regex, flags).test(composedAttribute);
}
/**
* @param {selectorParser.Attribute} selector
*/
function whitespaceSeparatedWordTest(selector) {
/**
* @param {string} value
*/
return value => {
if (selector.insensitive) {
var _selector$value;
return value.toLowerCase() === ((_selector$value = selector.value) === null || _selector$value === void 0 ? void 0 : _selector$value.toLowerCase());
}
return value === selector.value;
};
}
/**
* @param {string} operator
* @param {string} value
*/
function getRegexByOperator(operator, value) {
switch (operator) {
case '|=':
return `^${value}-?`;
case '^=':
return `^${value}`;
case '$=':
return `${value}$`;
case '*=':
default:
return `${value}`;
}
}
/**
* @param {postcss.ChildNode} node
* @param {selectorParser.Attribute} selector
*/
var getAttribute = ((node, selector) => {
var _selector$value2;
/** @type {postcss.ChildNode[]} */
const result = [];
const hasSelectorAttribute = (selector.attribute in node);
if (hasSelectorAttribute && (typeof selector.operator === 'undefined' || typeof selector.value === 'undefined')) {
result.push(node);
}
if (hasSelectorAttribute && selector.operator === '=' && selector.quoted && typeof selector.value !== 'undefined' && REGEX_REGEX.test(selector.value)) {
var _selector$value$match;
const [, regex, flags = ''] = (_selector$value$match = selector.value.match(REGEX_REGEX)) !== null && _selector$value$match !== void 0 ? _selector$value$match : [];
if (isRegexMatched(regex, flags, node, selector)) {
result.push(node);
}
}
/** @type {string} */
// @ts-ignore
const composedAttribute = node[selector.attribute];
if (hasSelectorAttribute && selector.operator === '=' && (composedAttribute === selector.value && !selector.insensitive || composedAttribute.toLowerCase() === ((_selector$value2 = selector.value) === null || _selector$value2 === void 0 ? void 0 : _selector$value2.toLowerCase()) && selector.insensitive)) {
result.push(node);
}
if (hasSelectorAttribute && selector.operator === '~=' && composedAttribute.split(' ').some(whitespaceSeparatedWordTest(selector))) {
result.push(node);
}
if (hasSelectorAttribute && typeof selector.operator !== 'undefined' && ['|=', '^=', '$=', '*='].includes(selector.operator)) {
var _selector$value3;
const flags = selector.insensitive ? 'i' : '';
const regex = getRegexByOperator(selector.operator, (_selector$value3 = selector.value) !== null && _selector$value3 !== void 0 ? _selector$value3 : '');
if (regex !== null && isRegexMatched(regex, flags, node, selector)) {
result.push(node);
}
}
return result;
});
/**
* @typedef {import('postcss').ChildNode} postcss.ChildNode
* @typedef {import('postcss-selector-parser').Pseudo} selectorParser.Pseudo
* @typedef {import('postcss-selector-parser').Root} selectorParser.Root
* @typedef {import('../../index').ProcessSelectors} ProcessSelectors
*/
/**
* @param {selectorParser.Pseudo} selector
* @param {postcss.ChildNode[]} siblings
*/
function processNthNodeQuery(selector, siblings) {
var _first$match;
const [first,, third] = selector.first.map(({
value
}) => value);
const [, negative = '', number] = (_first$match = first === null || first === void 0 ? void 0 : first.match(NTH_CHILD_QUERY_REGEX)) !== null && _first$match !== void 0 ? _first$match : [];
const parsedNumber = Number(number);
const parsedThird = Number(third);
const range = new Array(Number(negative !== '' ? parsedThird : siblings.length)).fill(0).map((value, index) => value + index);
const indices = range.map(index => {
if (negative !== '') {
return -(parsedNumber * index) + parsedThird;
}
return parsedNumber * index + parsedThird;
}).map(index => index - 1).filter(index => index >= 0 && index <= siblings.length - 1);
const uniqueIndices = [...new Set(indices)];
return uniqueIndices;
}
/**
* @param {postcss.ChildNode} node
* @param {selectorParser.Pseudo} selector
* @param {ProcessSelectors} processSelectors
*/
var getPseudo = ((node, selector, processSelectors) => {
var _node$parent, _node$parent2, _node$parent3;
const result = [];
if (selector.value === ':empty' && 'nodes' in node && node.nodes.length === 0) {
result.push(node);
}
if (selector.value === ':only-child' && ((_node$parent = node.parent) === null || _node$parent === void 0 ? void 0 : _node$parent.nodes.length) === 1 && node.parent.nodes[0] === node) {
result.push(node);
}
if (selector.value === ':first-child' && ((_node$parent2 = node.parent) === null || _node$parent2 === void 0 ? void 0 : _node$parent2.first) === node) {
result.push(node);
}
if (selector.value === ':last-child' && ((_node$parent3 = node.parent) === null || _node$parent3 === void 0 ? void 0 : _node$parent3.last) === node) {
result.push(node);
}
if (selector.value === ':nth-child' || selector.value === ':nth-last-child') {
var _node$parent$nodes, _node$parent4, _node$parent$nodes$le, _node$parent5;
const indices = processNthNodeQuery(selector, (_node$parent$nodes = (_node$parent4 = node.parent) === null || _node$parent4 === void 0 ? void 0 : _node$parent4.nodes) !== null && _node$parent$nodes !== void 0 ? _node$parent$nodes : []);
const nodesCount = ((_node$parent$nodes$le = (_node$parent5 = node.parent) === null || _node$parent5 === void 0 ? void 0 : _node$parent5.nodes.length) !== null && _node$parent$nodes$le !== void 0 ? _node$parent$nodes$le : 0) - 1;
indices.forEach(index => {
var _node$parent6;
const nodeIndex = selector.value === ':nth-child' ? index : nodesCount - index;
if (((_node$parent6 = node.parent) === null || _node$parent6 === void 0 ? void 0 : _node$parent6.nodes[nodeIndex]) === node) {
result.push(node);
}
});
}
if (selector.value === ':first-of-type' || selector.value === ':last-of-type' || selector.value === ':only-of-type' || selector.value === ':nth-of-type' || selector.value === ':nth-last-of-type') {
var _node$parent7;
/** @type {postcss.ChildNode[]} */
const typeResult = [];
(_node$parent7 = node.parent) === null || _node$parent7 === void 0 ? void 0 : _node$parent7.each(resolvedNode => {
if (resolvedNode.type === node.type) {
typeResult.push(resolvedNode);
}
});
const nodesCount = typeResult.length;
typeResult.forEach((resolvedTypeResultNode, index) => {
if (selector.value === ':first-of-type' && index === 0 && resolvedTypeResultNode === node) {
result.push(node);
}
if (selector.value === ':last-of-type' && index === nodesCount - 1 && resolvedTypeResultNode === node) {
result.push(node);
}
if (selector.value === ':only-of-type' && nodesCount === 1 && resolvedTypeResultNode === node) {
result.push(node);
}
});
if (selector.value === ':nth-of-type' || selector.value === ':nth-last-of-type') {
const indices = processNthNodeQuery(selector, typeResult);
const nthNodesCount = typeResult.length - 1;
indices.forEach(index => {
const nodeIndex = selector.value === ':nth-of-type' ? index : nthNodesCount - index;
if (typeResult[nodeIndex] === node) {
result.push(node);
}
});
}
}
if (selector.value === ':not' || selector.value === ':matches') {
const container = new PassthroughContainer();
container.append(node);
const processSelectorsResult = processSelectors(selector, container);
if (selector.value === ':not') {
if (!processSelectorsResult.includes(node)) {
result.push(node);
}
} else {
processSelectorsResult.forEach(resolvedProcessSelectorsResultNode => {
result.push(resolvedProcessSelectorsResultNode);
});
}
}
return result;
});
/* eslint-disable import/no-namespace */
/**
* @typedef {(
* selectors: selectorParser.Container|selectorParser.Root,
* ast: postcss.Root|postcss.ChildNode|PassthroughContainer
* ) => (postcss.Root|postcss.ChildNode|PassthroughContainer)[]} ProcessSelectors
*/
/**
* @type {ProcessSelectors}
*/
function processSelectors(selectors, ast) {
const nodes = selectors.map(rootSelector => {
if (!parser.isSelector(rootSelector)) {
return [];
}
return rootSelector.reduce((astContainer, selector) => {
return astContainer.map(node => {
if (node instanceof postcss.Root || node instanceof PassthroughContainer) {
switch (selector.type) {
case 'tag':
case 'universal':
return getTag(node, selector);
default:
return [];
}
}
switch (selector.type) {
case 'combinator':
return getCombinator(node, selector);
case 'attribute':
return getAttribute(node, selector);
case 'pseudo':
return getPseudo(node, selector, processSelectors);
default:
return [];
}
}).reduce(( /** @type {(postcss.ChildNode|PassthroughContainer)[]}*/array, result) => [...array, ...result], []).filter(result => result !== null);
}, [ast]);
}).reduce((array, result) => [...array, ...result], []);
const uniqueNodes = [...new Set(nodes)];
return uniqueNodes;
}
/**
* Queries PostCSS with CSS selector.
*
* @param {string} query CSS selector.
* @param {postcss.Root} ast PostCSS AST.
*/
async function index (query, ast) {
const selectorAst = await getSelectorAst(query);
const result = [];
for (const node of processSelectors(selectorAst, ast)) {
if (!(node instanceof PassthroughContainer)) {
result.push(node);
}
}
return result;
}
export { index as default };
//# sourceMappingURL=index.js.map