index.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. 'use strict';
  2. const report = require('../../utils/report');
  3. const ruleMessages = require('../../utils/ruleMessages');
  4. const transformSelector = require('../../utils/transformSelector');
  5. const validateOptions = require('../../utils/validateOptions');
  6. const { assertString } = require('../../utils/validateTypes');
  7. const ruleName = 'keyframe-selector-notation';
  8. const messages = ruleMessages(ruleName, {
  9. expected: (selector, fixedSelector) => `Expected "${selector}" to be "${fixedSelector}"`,
  10. });
  11. const meta = {
  12. url: 'https://stylelint.io/user-guide/rules/keyframe-selector-notation',
  13. fixable: true,
  14. };
  15. const PERCENTAGE_SELECTORS = new Set(['0%', '100%']);
  16. const KEYWORD_SELECTORS = new Set(['from', 'to']);
  17. const NAMED_TIMELINE_RANGE_SELECTORS = new Set(['cover', 'contain', 'entry', 'enter', 'exit']);
  18. const PERCENTAGE_TO_KEYWORD = new Map([
  19. ['0%', 'from'],
  20. ['100%', 'to'],
  21. ]);
  22. const KEYWORD_TO_PERCENTAGE = new Map([
  23. ['from', '0%'],
  24. ['to', '100%'],
  25. ]);
  26. /** @type {import('stylelint').Rule<'keyword' | 'percentage' | 'percentage-unless-within-keyword-only-block'>} */
  27. const rule = (primary, _, context) => {
  28. return (root, result) => {
  29. const validOptions = validateOptions(result, ruleName, {
  30. actual: primary,
  31. possible: ['keyword', 'percentage', 'percentage-unless-within-keyword-only-block'],
  32. });
  33. if (!validOptions) return;
  34. /**
  35. * @typedef {{
  36. * expFunc: (selector: string, selectorsInBlock: string[]) => boolean,
  37. * fixFunc: (selector: string) => string,
  38. * }} OptionFuncs
  39. *
  40. * @type {Record<primary, OptionFuncs>}
  41. */
  42. const optionFuncs = Object.freeze({
  43. keyword: {
  44. expFunc: (selector) => KEYWORD_SELECTORS.has(selector),
  45. fixFunc: (selector) => getFromMap(PERCENTAGE_TO_KEYWORD, selector),
  46. },
  47. percentage: {
  48. expFunc: (selector) => PERCENTAGE_SELECTORS.has(selector),
  49. fixFunc: (selector) => getFromMap(KEYWORD_TO_PERCENTAGE, selector),
  50. },
  51. 'percentage-unless-within-keyword-only-block': {
  52. expFunc: (selector, selectorsInBlock) => {
  53. if (selectorsInBlock.every((s) => KEYWORD_SELECTORS.has(s))) return true;
  54. return PERCENTAGE_SELECTORS.has(selector);
  55. },
  56. fixFunc: (selector) => getFromMap(KEYWORD_TO_PERCENTAGE, selector),
  57. },
  58. });
  59. root.walkAtRules(/^(-(moz|webkit)-)?keyframes$/i, (atRuleKeyframes) => {
  60. const selectorsInBlock =
  61. primary === 'percentage-unless-within-keyword-only-block'
  62. ? getSelectorsInBlock(atRuleKeyframes)
  63. : [];
  64. atRuleKeyframes.walkRules((keyframeRule) => {
  65. transformSelector(result, keyframeRule, (selectors) => {
  66. let first = true;
  67. selectors.walkTags((selectorTag) => {
  68. if (first && NAMED_TIMELINE_RANGE_SELECTORS.has(selectorTag.value)) {
  69. return false;
  70. }
  71. first = false;
  72. checkSelector(
  73. selectorTag.value,
  74. optionFuncs[primary],
  75. (fixedSelector) => (selectorTag.value = fixedSelector),
  76. );
  77. });
  78. });
  79. /**
  80. * @param {string} selector
  81. * @param {OptionFuncs} funcs
  82. * @param {(fixedSelector: string) => void} fixer
  83. */
  84. function checkSelector(selector, { expFunc, fixFunc }, fixer) {
  85. const normalizedSelector = selector.toLowerCase();
  86. if (
  87. !KEYWORD_SELECTORS.has(normalizedSelector) &&
  88. !PERCENTAGE_SELECTORS.has(normalizedSelector)
  89. ) {
  90. return;
  91. }
  92. if (expFunc(selector, selectorsInBlock)) return;
  93. const fixedSelector = fixFunc(selector);
  94. if (context.fix) {
  95. fixer(fixedSelector);
  96. return;
  97. }
  98. report({
  99. message: messages.expected,
  100. messageArgs: [selector, fixedSelector],
  101. node: keyframeRule,
  102. result,
  103. ruleName,
  104. word: selector,
  105. });
  106. }
  107. });
  108. });
  109. };
  110. };
  111. /**
  112. * @param {Map<string, string>} map
  113. * @param {string} key
  114. * @returns {string}
  115. */
  116. function getFromMap(map, key) {
  117. const value = map.get(key);
  118. assertString(value);
  119. return value;
  120. }
  121. /**
  122. * @param {import('postcss').AtRule} atRule
  123. * @returns {string[]}
  124. */
  125. function getSelectorsInBlock(atRule) {
  126. /** @type {string[]} */
  127. const selectors = [];
  128. atRule.walkRules((r) => {
  129. selectors.push(...r.selectors);
  130. });
  131. return selectors;
  132. }
  133. rule.ruleName = ruleName;
  134. rule.messages = messages;
  135. rule.meta = meta;
  136. module.exports = rule;