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`
  • Loading branch information
ybiquitous committed Jul 5, 2022
1 parent 6b08237 commit b96c8bd
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 10 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%'),
},
],
});
52 changes: 43 additions & 9 deletions lib/rules/keyframe-selector-notation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

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 ruleName = 'keyframe-selector-notation';

Expand All @@ -19,21 +19,23 @@ const meta = {
const PERCENTAGE_SELECTORS = new Set(['0%', '100%']);
const KEYWORD_SELECTORS = new Set(['from', 'to']);

/** @type {import('stylelint').Rule<'keyword' | 'percentage'>} */
/** @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: {
Expand All @@ -48,24 +50,41 @@ const rule = (primary, _, context) => {
return selector === 'from' ? '0%' : '100%';
},
},
'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) => {
return selector === 'from' ? '0%' : '100%';
},
},
});

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 {OptionFuncs} funcs
* @param {(fixedSelector: string) => string} fixer
*/
function checkSelector(selector, fixer) {
function checkSelector(selector, { expFunc, fixFunc }, fixer) {
const normalizedSelector = selector.toLowerCase();

if (
Expand All @@ -75,9 +94,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 +117,21 @@ const rule = (primary, _, context) => {
};
};

/**
* @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 b96c8bd

Please sign in to comment.