Skip to content

Commit

Permalink
Add percentage-unless-within-keyword-only-block primary option to `…
Browse files Browse the repository at this point in the history
…keyframe-selector-notation` (#6194)
  • Loading branch information
ybiquitous committed Jul 6, 2022
1 parent f491834 commit b52b8ca
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 17 deletions.
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

0 comments on commit b52b8ca

Please sign in to comment.