145 lines
3.8 KiB
JavaScript
145 lines
3.8 KiB
JavaScript
|
'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;
|
||
|
}
|