securityos/node_modules/stylelint/lib/utils/findMediaFeatureNames.js

145 lines
3.8 KiB
JavaScript
Raw Normal View History

2024-09-06 15:32:35 +00:00
'use strict';
const { TokenType, isToken, stringify, tokenize } = require('@csstools/css-tokenizer');
const {
isTokenNode,
parseCommaSeparatedListOfComponentValues,
isSimpleBlockNode,
} = require('@csstools/css-parser-algorithms');
const {
isGeneralEnclosed,
isMediaFeature,
isMediaQueryInvalid,
parseFromTokens,
} = require('@csstools/media-query-list-parser');
/** @typedef {Array<import('@csstools/media-query-list-parser').MediaQuery>} MediaQueryList */
/** @typedef {import('@csstools/css-tokenizer').TokenIdent} TokenIdent */
/** @typedef {{ stringify: () => string }} MediaQuerySerializer */
const rangeFeatureOperator = /[<>=]/;
/**
* Search a CSS string for Media Feature names.
* For every found name, invoke the callback, passing the token
* as an argument.
*
* Found tokens are mutable and modifications made to them will be reflected in the output.
*
* This function supports some non-standard syntaxes like SCSS variables and interpolation.
*
* @param {string} mediaQueryParams
* @param {(mediaFeatureName: TokenIdent) => void} callback
*
* @returns {MediaQuerySerializer}
*/
module.exports = function findMediaFeatureNames(mediaQueryParams, callback) {
const tokens = tokenize({ css: mediaQueryParams });
const list = parseCommaSeparatedListOfComponentValues(tokens);
const mediaQueryConditions = list.flatMap((listItem) => {
return listItem.flatMap((componentValue) => {
if (
!isSimpleBlockNode(componentValue) ||
componentValue.startToken[0] !== TokenType.OpenParen
) {
return [];
}
const blockTokens = componentValue.tokens();
const mediaQueryList = parseFromTokens(blockTokens, {
preserveInvalidMediaQueries: true,
});
return mediaQueryList.filter((mediaQuery) => {
return !isMediaQueryInvalid(mediaQuery);
});
});
});
mediaQueryConditions.forEach((mediaQuery) => {
mediaQuery.walk(({ node }) => {
if (isMediaFeature(node)) {
const token = node.getNameToken();
if (token[0] !== TokenType.Ident) return;
callback(token);
}
if (isGeneralEnclosed(node)) {
topLevelTokenNodes(node).forEach((token, i, topLevelTokens) => {
if (token[0] !== TokenType.Ident) {
return;
}
const nextToken = topLevelTokens[i + 1];
const prevToken = topLevelTokens[i - 1];
if (
// Media Feature
(!prevToken && nextToken && nextToken[0] === TokenType.Colon) ||
// Range Feature
(nextToken &&
nextToken[0] === TokenType.Delim &&
rangeFeatureOperator.test(nextToken[4].value)) ||
// Range Feature
(prevToken &&
prevToken[0] === TokenType.Delim &&
rangeFeatureOperator.test(prevToken[4].value))
) {
callback(token);
}
});
}
});
});
// Serializing takes time/resources and not all callers will use this.
// By returning an object with a stringify method, we can avoid doing
// this work when it's not needed.
return {
stringify() {
return stringify(...tokens);
},
};
};
/** @param {import('@csstools/media-query-list-parser').GeneralEnclosed} node */
function topLevelTokenNodes(node) {
const components = node.value.value;
if (isToken(components) || components.length === 0 || isToken(components[0])) {
return [];
}
/** @type {Array<import('@csstools/css-tokenizer').CSSToken>} */
const relevantTokens = [];
// To consume the next token if it is a scss variable
let lastWasDollarSign = false;
components.forEach((component) => {
// Only preserve top level tokens (idents, delims, ...)
// Discard all blocks, functions, ...
if (component && isTokenNode(component)) {
if (component.value[0] === TokenType.Delim && component.value[4].value === '$') {
lastWasDollarSign = true;
return;
}
if (lastWasDollarSign) {
lastWasDollarSign = false;
return;
}
relevantTokens.push(component.value);
}
});
return relevantTokens;
}