index.mjs 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import { assertString } from '../../utils/validateTypes.mjs';
  2. import { keyframeSelectorKeywords } from '../../reference/keywords.mjs';
  3. import { atRuleRegexes } from '../../utils/regexes.mjs';
  4. import getRuleSelector from '../../utils/getRuleSelector.mjs';
  5. import parseSelector from '../../utils/parseSelector.mjs';
  6. import parser from 'postcss-selector-parser';
  7. import report from '../../utils/report.mjs';
  8. import ruleMessages from '../../utils/ruleMessages.mjs';
  9. import validateOptions from '../../utils/validateOptions.mjs';
  10. const ruleName = 'keyframe-selector-notation';
  11. const messages = ruleMessages(ruleName, {
  12. expected: (selector, fixedSelector) => `Expected "${selector}" to be "${fixedSelector}"`,
  13. });
  14. const meta = {
  15. url: 'https://stylelint.io/user-guide/rules/keyframe-selector-notation',
  16. fixable: true,
  17. };
  18. const PERCENTAGE_SELECTORS = new Set(['0%', '100%']);
  19. const PERCENTAGE_TO_KEYWORD = new Map([
  20. ['0%', 'from'],
  21. ['100%', 'to'],
  22. ]);
  23. const KEYWORD_TO_PERCENTAGE = new Map([
  24. ['from', '0%'],
  25. ['to', '100%'],
  26. ]);
  27. /** @type {import('stylelint').CoreRules[ruleName]} */
  28. const rule = (primary) => {
  29. return (root, result) => {
  30. const validOptions = validateOptions(result, ruleName, {
  31. actual: primary,
  32. possible: ['keyword', 'percentage', 'percentage-unless-within-keyword-only-block'],
  33. });
  34. if (!validOptions) return;
  35. /**
  36. * @typedef {{
  37. * expFunc: (selector: string, selectorsInBlock: string[]) => boolean,
  38. * fixFunc: (selector: string) => string,
  39. * }} OptionFuncs
  40. *
  41. * @type {Record<typeof primary, OptionFuncs>}
  42. */
  43. const optionFuncs = {
  44. keyword: {
  45. expFunc: (selector) => keyframeSelectorKeywords.has(selector),
  46. fixFunc: (selector) => getFromMap(PERCENTAGE_TO_KEYWORD, selector),
  47. },
  48. percentage: {
  49. expFunc: (selector) => PERCENTAGE_SELECTORS.has(selector),
  50. fixFunc: (selector) => getFromMap(KEYWORD_TO_PERCENTAGE, selector),
  51. },
  52. 'percentage-unless-within-keyword-only-block': {
  53. expFunc: (selector, selectorsInBlock) => {
  54. if (selectorsInBlock.every((s) => keyframeSelectorKeywords.has(s))) return true;
  55. return PERCENTAGE_SELECTORS.has(selector);
  56. },
  57. fixFunc: (selector) => getFromMap(KEYWORD_TO_PERCENTAGE, selector),
  58. },
  59. };
  60. const { expFunc, fixFunc } = optionFuncs[primary];
  61. root.walkAtRules(atRuleRegexes.keyframesName, (atRuleKeyframes) => {
  62. const selectorsInBlock =
  63. primary === 'percentage-unless-within-keyword-only-block'
  64. ? getSelectorsInBlock(atRuleKeyframes)
  65. : [];
  66. atRuleKeyframes.walkRules((keyframeRule) => {
  67. const selectors = parseSelector(getRuleSelector(keyframeRule), result, keyframeRule);
  68. if (!selectors) return;
  69. selectors.each((selector) => {
  70. const parsed = parseKeyframeSelector(selector);
  71. if (!parsed) return;
  72. const [keyframeSelector, normalizedSelector] = parsed;
  73. if (expFunc(normalizedSelector, selectorsInBlock)) return;
  74. const fixedSelector = fixFunc(normalizedSelector);
  75. const fix = () => {
  76. keyframeSelector.value = fixedSelector;
  77. keyframeRule.selector = selectors.toString();
  78. };
  79. report({
  80. message: messages.expected,
  81. messageArgs: [keyframeSelector.value, fixedSelector],
  82. node: keyframeRule,
  83. result,
  84. ruleName,
  85. index: keyframeSelector.sourceIndex,
  86. endIndex: keyframeSelector.sourceIndex + normalizedSelector.length,
  87. fix: {
  88. apply: fix,
  89. node: keyframeRule,
  90. },
  91. });
  92. });
  93. });
  94. });
  95. };
  96. };
  97. /** @import { Selector, Tag } from 'postcss-selector-parser' */
  98. /**
  99. * Full syntax is:
  100. * `<keyframe-selector> = from | to | <percentage [0,100]> | <timeline-range-name> <percentage>`
  101. *
  102. * Only parses `from | to | <percentage [0,100]>` and returns `undefined` in any other case.
  103. *
  104. * @param {Selector} selector
  105. * @returns {[Tag,string]|undefined}
  106. */
  107. function parseKeyframeSelector(selector) {
  108. const relevantNodes = selector.nodes.filter((node) => {
  109. if (parser.isComment(node)) return false;
  110. if (parser.isCombinator(node) && node.value.trim() === '') return false;
  111. return true;
  112. });
  113. if (relevantNodes.length !== 1) return;
  114. if (!relevantNodes.every(parser.isTag)) return;
  115. const keyframeSelector = relevantNodes[0];
  116. if (!keyframeSelector) return;
  117. const normalizedSelector = keyframeSelector.value.toLowerCase();
  118. if (
  119. !keyframeSelectorKeywords.has(normalizedSelector) &&
  120. !PERCENTAGE_SELECTORS.has(normalizedSelector)
  121. ) {
  122. return;
  123. }
  124. return [keyframeSelector, normalizedSelector];
  125. }
  126. /**
  127. * @param {Map<string, string>} map
  128. * @param {string} key
  129. * @returns {string}
  130. */
  131. function getFromMap(map, key) {
  132. const value = map.get(key);
  133. assertString(value);
  134. return value;
  135. }
  136. /**
  137. * @param {import('postcss').AtRule} atRule
  138. * @returns {string[]}
  139. */
  140. function getSelectorsInBlock(atRule) {
  141. /** @type {string[]} */
  142. const selectors = [];
  143. atRule.walkRules((r) => {
  144. selectors.push(...r.selectors.map((selector) => selector.toLowerCase()));
  145. });
  146. return selectors;
  147. }
  148. rule.ruleName = ruleName;
  149. rule.messages = messages;
  150. rule.meta = meta;
  151. export default rule;