findMediaFeatureNames.cjs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. // NOTICE: This file is generated by Rollup. To modify it,
  2. // please instead edit the ESM counterpart and rebuild with Rollup (npm run build).
  3. 'use strict';
  4. const cssTokenizer = require('@csstools/css-tokenizer');
  5. const mediaQueryListParser = require('@csstools/media-query-list-parser');
  6. const cssParserAlgorithms = require('@csstools/css-parser-algorithms');
  7. /** @typedef {Array<import('@csstools/media-query-list-parser').MediaQuery>} MediaQueryList */
  8. /** @typedef {import('@csstools/css-tokenizer').TokenIdent} TokenIdent */
  9. /** @typedef {{ stringify: () => string }} MediaQuerySerializer */
  10. const rangeFeatureOperator = /[<>=]/;
  11. /**
  12. * Search a CSS string for Media Feature names.
  13. * For every found name, invoke the callback, passing the token
  14. * as an argument.
  15. *
  16. * Found tokens are mutable and modifications made to them will be reflected in the output.
  17. *
  18. * This function supports some non-standard syntaxes like SCSS variables and interpolation.
  19. *
  20. * @param {string} mediaQueryParams
  21. * @param {(mediaFeatureName: TokenIdent) => void} callback
  22. *
  23. * @returns {MediaQuerySerializer}
  24. */
  25. function findMediaFeatureNames(mediaQueryParams, callback) {
  26. const tokens = cssTokenizer.tokenize({ css: mediaQueryParams });
  27. const list = cssParserAlgorithms.parseCommaSeparatedListOfComponentValues(tokens);
  28. const mediaQueryConditions = list.flatMap((listItem) => {
  29. return listItem.flatMap((componentValue) => {
  30. if (
  31. !cssParserAlgorithms.isSimpleBlockNode(componentValue) ||
  32. componentValue.startToken[0] !== cssTokenizer.TokenType.OpenParen
  33. ) {
  34. return [];
  35. }
  36. const blockTokens = componentValue.tokens();
  37. const mediaQueryList = mediaQueryListParser.parseFromTokens(blockTokens, {
  38. preserveInvalidMediaQueries: true,
  39. });
  40. return mediaQueryList.filter((mediaQuery) => {
  41. return !mediaQueryListParser.isMediaQueryInvalid(mediaQuery);
  42. });
  43. });
  44. });
  45. mediaQueryConditions.forEach((mediaQuery) => {
  46. mediaQuery.walk(({ node }) => {
  47. if (mediaQueryListParser.isMediaFeature(node)) {
  48. const token = node.getNameToken();
  49. if (token[0] !== cssTokenizer.TokenType.Ident) return;
  50. callback(token);
  51. }
  52. if (mediaQueryListParser.isGeneralEnclosed(node)) {
  53. topLevelTokenNodes(node).forEach((token, i, topLevelTokens) => {
  54. if (token[0] !== cssTokenizer.TokenType.Ident) {
  55. return;
  56. }
  57. const nextToken = topLevelTokens[i + 1];
  58. const prevToken = topLevelTokens[i - 1];
  59. if (
  60. // Media Feature
  61. (!prevToken && nextToken && nextToken[0] === cssTokenizer.TokenType.Colon) ||
  62. // Range Feature
  63. (nextToken &&
  64. nextToken[0] === cssTokenizer.TokenType.Delim &&
  65. rangeFeatureOperator.test(nextToken[4].value)) ||
  66. // Range Feature
  67. (prevToken &&
  68. prevToken[0] === cssTokenizer.TokenType.Delim &&
  69. rangeFeatureOperator.test(prevToken[4].value))
  70. ) {
  71. callback(token);
  72. }
  73. });
  74. }
  75. });
  76. });
  77. // Serializing takes time/resources and not all callers will use this.
  78. // By returning an object with a stringify method, we can avoid doing
  79. // this work when it's not needed.
  80. return {
  81. stringify() {
  82. return cssTokenizer.stringify(...tokens);
  83. },
  84. };
  85. }
  86. /** @param {import('@csstools/media-query-list-parser').GeneralEnclosed} node */
  87. function topLevelTokenNodes(node) {
  88. const components = node.value.value;
  89. if (cssTokenizer.isToken(components) || components.length === 0 || cssTokenizer.isToken(components[0])) {
  90. return [];
  91. }
  92. /** @type {Array<import('@csstools/css-tokenizer').CSSToken>} */
  93. const relevantTokens = [];
  94. // To consume the next token if it is a scss variable
  95. let lastWasDollarSign = false;
  96. components.forEach((component) => {
  97. // Only preserve top level tokens (idents, delims, ...)
  98. // Discard all blocks, functions, ...
  99. if (component && cssParserAlgorithms.isTokenNode(component)) {
  100. if (component.value[0] === cssTokenizer.TokenType.Delim && component.value[4].value === '$') {
  101. lastWasDollarSign = true;
  102. return;
  103. }
  104. if (lastWasDollarSign) {
  105. lastWasDollarSign = false;
  106. return;
  107. }
  108. relevantTokens.push(component.value);
  109. }
  110. });
  111. return relevantTokens;
  112. }
  113. module.exports = findMediaFeatureNames;