'use strict'; const report = require('../../utils/report'); const ruleMessages = require('../../utils/ruleMessages'); const transformSelector = require('../../utils/transformSelector'); const validateOptions = require('../../utils/validateOptions'); const { assertString } = require('../../utils/validateTypes'); const ruleName = 'keyframe-selector-notation'; const messages = ruleMessages(ruleName, { expected: (selector, fixedSelector) => `Expected "${selector}" to be "${fixedSelector}"`, }); const meta = { url: 'https://stylelint.io/user-guide/rules/keyframe-selector-notation', fixable: true, }; const PERCENTAGE_SELECTORS = new Set(['0%', '100%']); const KEYWORD_SELECTORS = new Set(['from', 'to']); const NAMED_TIMELINE_RANGE_SELECTORS = new Set(['cover', 'contain', 'entry', 'enter', 'exit']); const PERCENTAGE_TO_KEYWORD = new Map([ ['0%', 'from'], ['100%', 'to'], ]); const KEYWORD_TO_PERCENTAGE = new Map([ ['from', '0%'], ['to', '100%'], ]); /** @type {import('stylelint').Rule<'keyword' | 'percentage' | 'percentage-unless-within-keyword-only-block'>} */ const rule = (primary, _, context) => { return (root, result) => { const validOptions = validateOptions(result, ruleName, { actual: primary, possible: ['keyword', 'percentage', 'percentage-unless-within-keyword-only-block'], }); if (!validOptions) return; /** * @typedef {{ * expFunc: (selector: string, selectorsInBlock: string[]) => boolean, * fixFunc: (selector: string) => string, * }} OptionFuncs * * @type {Record} */ const optionFuncs = Object.freeze({ keyword: { expFunc: (selector) => KEYWORD_SELECTORS.has(selector), fixFunc: (selector) => getFromMap(PERCENTAGE_TO_KEYWORD, selector), }, percentage: { expFunc: (selector) => PERCENTAGE_SELECTORS.has(selector), fixFunc: (selector) => getFromMap(KEYWORD_TO_PERCENTAGE, selector), }, 'percentage-unless-within-keyword-only-block': { expFunc: (selector, selectorsInBlock) => { if (selectorsInBlock.every((s) => KEYWORD_SELECTORS.has(s))) return true; return PERCENTAGE_SELECTORS.has(selector); }, fixFunc: (selector) => getFromMap(KEYWORD_TO_PERCENTAGE, selector), }, }); root.walkAtRules(/^(-(moz|webkit)-)?keyframes$/i, (atRuleKeyframes) => { const selectorsInBlock = primary === 'percentage-unless-within-keyword-only-block' ? getSelectorsInBlock(atRuleKeyframes) : []; atRuleKeyframes.walkRules((keyframeRule) => { transformSelector(result, keyframeRule, (selectors) => { let first = true; selectors.walkTags((selectorTag) => { if (first && NAMED_TIMELINE_RANGE_SELECTORS.has(selectorTag.value)) { return false; } first = false; checkSelector( selectorTag.value, optionFuncs[primary], (fixedSelector) => (selectorTag.value = fixedSelector), ); }); }); /** * @param {string} selector * @param {OptionFuncs} funcs * @param {(fixedSelector: string) => void} fixer */ function checkSelector(selector, { expFunc, fixFunc }, fixer) { const normalizedSelector = selector.toLowerCase(); if ( !KEYWORD_SELECTORS.has(normalizedSelector) && !PERCENTAGE_SELECTORS.has(normalizedSelector) ) { return; } if (expFunc(selector, selectorsInBlock)) return; const fixedSelector = fixFunc(selector); if (context.fix) { fixer(fixedSelector); return; } report({ message: messages.expected, messageArgs: [selector, fixedSelector], node: keyframeRule, result, ruleName, word: selector, }); } }); }); }; }; /** * @param {Map} map * @param {string} key * @returns {string} */ function getFromMap(map, key) { const value = map.get(key); assertString(value); return value; } /** * @param {import('postcss').AtRule} atRule * @returns {string[]} */ function getSelectorsInBlock(atRule) { /** @type {string[]} */ const selectors = []; atRule.walkRules((r) => { selectors.push(...r.selectors); }); return selectors; } rule.ruleName = ruleName; rule.messages = messages; rule.meta = meta; module.exports = rule;