Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add percentage-unless-within-keyword-only-block primary option to keyframe-selector-notation #6194

Merged
merged 2 commits into from
Jul 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 24 additions & 1 deletion lib/rules/keyframe-selector-notation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The [`fix` option](../../../docs/user-guide/usage/options.md#fix) can automatica

## Options

`string`: `"keyword"|"percentage"`
`string`: `"keyword"|"percentage"|"percentage-unless-within-keyword-only-block"`

### `"keyword"`

Expand Down Expand Up @@ -52,3 +52,26 @@ The following pattern is _not_ considered a problem:
```css
@keyframes foo { 0% {} 100% {} }
```

### `"percentage-unless-within-keyword-only-block"`

Keyframe selectors _must_ use the percentage notation unless within a keyword-only block.

The following pattern is considered a problem:

<!-- prettier-ignore -->
```css
@keyframes foo { from {} 100% {} }
```

The following pattern are _not_ considered problems:

<!-- prettier-ignore -->
```css
@keyframes foo { 0% {} 100% {} }
```

<!-- prettier-ignore -->
```css
@keyframes foo { from {} to {} }
```
59 changes: 59 additions & 0 deletions lib/rules/keyframe-selector-notation/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,62 @@ testRule({
},
],
});

testRule({
ruleName,
config: ['percentage-unless-within-keyword-only-block'],
fix: true,

accept: [
{
code: '@keyframes foo { 0% {} 100% {} }',
},
{
code: '@keyframes foo { 0% {} 20%,80% {} 100% {} }',
},
{
code: '@keyframes foo { 0%,100% {} }',
},
{
code: '@keyframes foo { from {} to {} }',
},
{
code: '@keyframes foo { from,to {} }',
},
],

reject: [
{
code: '@keyframes foo { from {} 100% {} }',
fixed: '@keyframes foo { 0% {} 100% {} }',
message: messages.expected('from', '0%'),
},
{
code: '@keyframes foo { 0% {} to {} }',
fixed: '@keyframes foo { 0% {} 100% {} }',
message: messages.expected('to', '100%'),
},
{
code: '@keyframes foo { from {} 20%,80% {} to {} }',
fixed: '@keyframes foo { 0% {} 20%,80% {} 100% {} }',
warnings: [
{
message: messages.expected('from', '0%'),
},
{
message: messages.expected('to', '100%'),
},
],
},
{
code: '@keyframes foo { from,100% {} }',
fixed: '@keyframes foo { 0%,100% {} }',
message: messages.expected('from', '0%'),
},
{
code: '@keyframes foo { 0%,to {} }',
fixed: '@keyframes foo { 0%,100% {} }',
message: messages.expected('to', '100%'),
},
],
});
82 changes: 66 additions & 16 deletions lib/rules/keyframe-selector-notation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const transformSelector = require('../../utils/transformSelector');
const validateOptions = require('../../utils/validateOptions');
const { assertString } = require('../../utils/validateTypes');

const ruleName = 'keyframe-selector-notation';

Expand All @@ -18,54 +19,75 @@ const meta = {

const PERCENTAGE_SELECTORS = new Set(['0%', '100%']);
const KEYWORD_SELECTORS = new Set(['from', 'to']);

/** @type {import('stylelint').Rule<'keyword' | 'percentage'>} */
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'],
possible: ['keyword', 'percentage', 'percentage-unless-within-keyword-only-block'],
});

if (!validOptions) return;

/**
* @type {Record<primary, {
* expFunc: (selector: string) => boolean,
* @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) => {
return selector === '0%' ? 'from' : 'to';
},
fixFunc: (selector) => getFromMap(PERCENTAGE_TO_KEYWORD, selector),
},
percentage: {
expFunc: (selector) => PERCENTAGE_SELECTORS.has(selector),
fixFunc: (selector) => {
return selector === 'from' ? '0%' : '100%';
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) => {
selectors.walkTags((selectorTag) => {
checkSelector(
selectorTag.value,
optionFuncs[primary],
(fixedSelector) => (selectorTag.value = fixedSelector),
);
});
});

/**
* @param {string} selector
* @param {(fixedSelector: string) => string} fixer
* @param {OptionFuncs} funcs
* @param {(fixedSelector: string) => void} fixer
*/
function checkSelector(selector, fixer) {
function checkSelector(selector, { expFunc, fixFunc }, fixer) {
const normalizedSelector = selector.toLowerCase();

if (
Expand All @@ -75,9 +97,9 @@ const rule = (primary, _, context) => {
return;
}

if (optionFuncs[primary].expFunc(selector)) return;
if (expFunc(selector, selectorsInBlock)) return;

const fixedSelector = optionFuncs[primary].fixFunc(selector);
const fixedSelector = fixFunc(selector);

if (context.fix) {
fixer(fixedSelector);
Expand All @@ -98,6 +120,34 @@ const rule = (primary, _, context) => {
};
};

/**
* @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;
Expand Down