index.cjs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  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 mediaQueryListParser = require('@csstools/media-query-list-parser');
  5. const cssParserAlgorithms = require('@csstools/css-parser-algorithms');
  6. const cssTokenizer = require('@csstools/css-tokenizer');
  7. const nodeFieldIndices = require('../../utils/nodeFieldIndices.cjs');
  8. const regexes = require('../../utils/regexes.cjs');
  9. const optionsMatches = require('../../utils/optionsMatches.cjs');
  10. const parseMediaQuery = require('../../utils/parseMediaQuery.cjs');
  11. const mediaFeatures = require('../../reference/mediaFeatures.cjs');
  12. const report = require('../../utils/report.cjs');
  13. const ruleMessages = require('../../utils/ruleMessages.cjs');
  14. const validateOptions = require('../../utils/validateOptions.cjs');
  15. const ruleName = 'media-feature-range-notation';
  16. const messages = ruleMessages(ruleName, {
  17. expected: (primary) => `Expected "${primary}" media feature range notation`,
  18. });
  19. const meta = {
  20. url: 'https://stylelint.io/user-guide/rules/media-feature-range-notation',
  21. fixable: true,
  22. };
  23. /** @import {TokenDelim} from '@csstools/css-tokenizer' */
  24. /** @type {import('stylelint').CoreRules[ruleName]} */
  25. const rule = (primary, secondaryOptions) => {
  26. return (root, result) => {
  27. const validOptions = validateOptions(
  28. result,
  29. ruleName,
  30. {
  31. actual: primary,
  32. possible: ['prefix', 'context'],
  33. },
  34. {
  35. actual: secondaryOptions,
  36. possible: {
  37. except: ['exact-value'],
  38. },
  39. optional: true,
  40. },
  41. );
  42. if (!validOptions) {
  43. return;
  44. }
  45. const exceptExactValue = optionsMatches(secondaryOptions, 'except', 'exact-value');
  46. root.walkAtRules(regexes.atRuleRegexes.mediaName, (atRule) => {
  47. const mediaQueryList = parseMediaQuery(atRule);
  48. mediaQueryList.forEach((mediaQuery) => {
  49. if (mediaQueryListParser.isMediaQueryInvalid(mediaQuery)) return;
  50. mediaQuery.walk(({ node, parent }) => {
  51. // Only look at plain and range notation media features
  52. if (!mediaQueryListParser.isMediaFeatureRange(node) && !mediaQueryListParser.isMediaFeaturePlain(node)) return;
  53. const featureName = node.getName();
  54. const unprefixedMediaFeature = featureName.replace(/^(?:min|max)-/i, '');
  55. if (!mediaFeatures.rangeTypeMediaFeatureNames.has(unprefixedMediaFeature)) return;
  56. if (exceptExactValue) {
  57. const isMediaFeatureRangeWithExactOperator =
  58. (mediaQueryListParser.isMediaFeatureRangeNameValue(node) || mediaQueryListParser.isMediaFeatureRangeValueName(node)) &&
  59. node.operator.length === 1 &&
  60. node.operator[0][1] === '=';
  61. const isMediaFeaturePlainUnprefixed =
  62. mediaQueryListParser.isMediaFeaturePlain(node) && featureName.length === unprefixedMediaFeature.length;
  63. if (isMediaFeatureRangeWithExactOperator || isMediaFeaturePlainUnprefixed) {
  64. primary = primary === 'prefix' ? 'context' : 'prefix';
  65. }
  66. }
  67. // Expected plain notation and received plain notation
  68. if (primary === 'prefix' && mediaQueryListParser.isMediaFeaturePlain(node)) return;
  69. // Expected range notation and received range notation
  70. if (primary === 'context' && mediaQueryListParser.isMediaFeatureRange(node)) return;
  71. /**
  72. * @param {object} entry
  73. * @param {import('@csstools/media-query-list-parser').MediaFeaturePlain} entry.node
  74. * @param {import('@csstools/media-query-list-parser').MediaFeature} entry.parent
  75. */
  76. const contextFixer = (entry) => () => {
  77. /** @type {[TokenDelim]|[TokenDelim, TokenDelim]} */
  78. let operator;
  79. if (/^min-/i.test(featureName)) {
  80. operator = [
  81. [cssTokenizer.TokenType.Delim, '>', -1, -1, { value: '>' }],
  82. [cssTokenizer.TokenType.Delim, '=', -1, -1, { value: '=' }],
  83. ];
  84. } else if (/^max-/i.test(featureName)) {
  85. operator = [
  86. [cssTokenizer.TokenType.Delim, '<', -1, -1, { value: '<' }],
  87. [cssTokenizer.TokenType.Delim, '=', -1, -1, { value: '=' }],
  88. ];
  89. } else {
  90. operator = [[cssTokenizer.TokenType.Delim, '=', -1, -1, { value: '=' }]];
  91. }
  92. entry.parent.feature = new mediaQueryListParser.MediaFeatureRangeNameValue(
  93. new mediaQueryListParser.MediaFeatureName(
  94. new cssParserAlgorithms.TokenNode([
  95. cssTokenizer.TokenType.Ident,
  96. unprefixedMediaFeature,
  97. -1,
  98. -1,
  99. { value: unprefixedMediaFeature },
  100. ]),
  101. entry.node.name.before,
  102. entry.node.name.after.length > 0
  103. ? entry.node.name.after
  104. : [[cssTokenizer.TokenType.Whitespace, ' ', -1, -1, undefined]],
  105. ),
  106. operator,
  107. entry.node.value,
  108. );
  109. const expectedMediaQueryList = mediaQueryList.map((mq) => mq.toString()).join(',');
  110. if (expectedMediaQueryList === atRule.params) return;
  111. atRule.params = expectedMediaQueryList;
  112. };
  113. const hasFix =
  114. primary === 'context' && mediaQueryListParser.isMediaFeaturePlain(node) && mediaQueryListParser.isMediaFeature(parent);
  115. const fix = hasFix ? contextFixer({ node, parent }) : undefined;
  116. const [startIndex, endIndex] = cssParserAlgorithms.sourceIndices(node);
  117. const atRuleIndex = nodeFieldIndices.atRuleParamIndex(atRule);
  118. report({
  119. message: messages.expected,
  120. messageArgs: [primary],
  121. node: atRule,
  122. index: atRuleIndex + startIndex - 1,
  123. endIndex: atRuleIndex + endIndex + 1 + 1,
  124. result,
  125. ruleName,
  126. fix: {
  127. apply: fix,
  128. node: atRule,
  129. },
  130. });
  131. });
  132. });
  133. });
  134. };
  135. };
  136. rule.ruleName = ruleName;
  137. rule.messages = messages;
  138. rule.meta = meta;
  139. module.exports = rule;