securityos/node_modules/stylelint/lib/rules/keyframe-selector-notation/index.js

165 lines
4.2 KiB
JavaScript

'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<primary, OptionFuncs>}
*/
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<string, string>} 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;