index.js 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. 'use strict';
  2. const report = require('../../utils/report');
  3. const ruleMessages = require('../../utils/ruleMessages');
  4. const validateOptions = require('../../utils/validateOptions');
  5. const { TokenType } = require('@csstools/css-tokenizer');
  6. const { isTokenNode } = require('@csstools/css-parser-algorithms');
  7. const { isMediaFeaturePlain, isMediaFeatureRange } = require('@csstools/media-query-list-parser');
  8. const validateObjectWithArrayProps = require('../../utils/validateObjectWithArrayProps');
  9. const { isString } = require('../../utils/validateTypes');
  10. const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp');
  11. const atRuleParamIndex = require('../../utils/atRuleParamIndex');
  12. const parseMediaQuery = require('../../utils/parseMediaQuery');
  13. const ruleName = 'media-feature-name-unit-allowed-list';
  14. const messages = ruleMessages(ruleName, {
  15. rejected: (unit, name) => `Unexpected unit "${unit}" for name "${name}"`,
  16. });
  17. const meta = {
  18. url: 'https://stylelint.io/user-guide/rules/media-feature-name-unit-allowed-list',
  19. };
  20. /** @type {import('stylelint').Rule<Record<string, string | string[]>>} */
  21. const rule = (primary) => {
  22. return (root, result) => {
  23. const validOptions = validateOptions(result, ruleName, {
  24. actual: primary,
  25. possible: [validateObjectWithArrayProps(isString)],
  26. });
  27. if (!validOptions) {
  28. return;
  29. }
  30. const primaryPairs = Object.entries(primary);
  31. const primaryUnitList = (/** @type {string} */ featureName) => {
  32. for (const [name, unit] of primaryPairs) {
  33. if (matchesStringOrRegExp(featureName, name)) return [unit].flat();
  34. }
  35. return undefined;
  36. };
  37. root.walkAtRules(/^media$/i, (atRule) => {
  38. const mediaQueryList = parseMediaQuery(atRule, result);
  39. mediaQueryList.forEach((mediaQuery) => {
  40. mediaQuery.walk((entry) => {
  41. if (!isMediaFeaturePlain(entry.node) && !isMediaFeatureRange(entry.node)) {
  42. return;
  43. }
  44. const featureName = entry.node.getName();
  45. const unitList = primaryUnitList(featureName);
  46. if (!unitList) {
  47. return;
  48. }
  49. entry.node.walk(({ node: childNode }) => {
  50. if (!isTokenNode(childNode)) {
  51. return;
  52. }
  53. const [tokenType, , startIndex, endIndex, parsedValue] = childNode.value;
  54. if (tokenType !== TokenType.Dimension) {
  55. return;
  56. }
  57. if (unitList.includes(parsedValue.unit.toLowerCase())) {
  58. return;
  59. }
  60. const atRuleIndex = atRuleParamIndex(atRule);
  61. report({
  62. message: messages.rejected(parsedValue.unit, featureName),
  63. node: atRule,
  64. index: atRuleIndex + startIndex,
  65. endIndex: atRuleIndex + endIndex + 1,
  66. result,
  67. ruleName,
  68. });
  69. });
  70. });
  71. });
  72. });
  73. };
  74. };
  75. rule.ruleName = ruleName;
  76. rule.messages = messages;
  77. rule.meta = meta;
  78. module.exports = rule;