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 keyframe-selector-notation #6164

Merged
merged 7 commits into from Jun 23, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions docs/user-guide/rules/list.md
Expand Up @@ -134,6 +134,10 @@ Within each cateogory, the rules are grouped by the [_thing_](http://apps.workfl

- [`import-notation`](../../../lib/rules/import-notation/README.md): Specify string or URL notation for `@import` rules (Autofixable).

### Keyframe selectors
mattxwang marked this conversation as resolved.
Show resolved Hide resolved

- [`keyframe-selector-notation`](../../../lib/rules/keyframe-selector-notation/README.md): Specify keyword or percentage notation for keyframe selectors.

### Keyframes

- [`keyframes-name-pattern`](../../../lib/rules/keyframes-name-pattern/README.md): Specify a pattern for keyframe names.
Expand Down
1 change: 1 addition & 0 deletions lib/rules/index.js
Expand Up @@ -122,6 +122,7 @@ const rules = {
'import-notation': importLazy('./import-notation'),
'keyframe-block-no-duplicate-selectors': importLazy('./keyframe-block-no-duplicate-selectors'),
'keyframe-declaration-no-important': importLazy('./keyframe-declaration-no-important'),
'keyframe-selector-notation': importLazy('./keyframe-selector-notation'),
'keyframes-name-pattern': importLazy('./keyframes-name-pattern'),
'length-zero-no-unit': importLazy('./length-zero-no-unit'),
linebreaks: importLazy('./linebreaks'),
Expand Down
54 changes: 54 additions & 0 deletions lib/rules/keyframe-selector-notation/README.md
@@ -0,0 +1,54 @@
# keyframe-selector-notation

Specify keyword or percentage notation for keyframe selectors. This rule only enforces the convention for `from`, `to`, `0%` and `100%`.
mattxwang marked this conversation as resolved.
Show resolved Hide resolved

<!-- prettier-ignore -->
```css
@keyframes foo { from {} to {} }
/** ↑ ↑
* These notations */
```

The keyword `from` is equivalent to the value `0%`. The keyword `to` is equivalent to the value `100%`.

The [`fix` option](../../../docs/user-guide/usage/options.md#fix) can automatically fix all of the problems reported by this rule.

## Options

`string`: `"keyword"|"percentage"`

### `"keyword"`

Keyframe selectors _must always_ use the keyword notation.

The following pattern is considered a problem:

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

The following pattern is _not_ considered a problem:

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

### `"percentage"`

Keyframe selectors _must always_ use the percentage notation.

The following pattern is considered a problem:

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

The following pattern is _not_ considered a problem:

<!-- prettier-ignore -->
```css
@keyframes foo { 0 {} 100% {} }
mattxwang marked this conversation as resolved.
Show resolved Hide resolved
```
121 changes: 121 additions & 0 deletions lib/rules/keyframe-selector-notation/__tests__/index.js
@@ -0,0 +1,121 @@
'use strict';

const { messages, ruleName } = require('..');

testRule({
ruleName,
config: ['keyword'],
fix: true,

accept: [
{
code: '@keyframes foo { from {} }',
},
{
code: '@keyframes foo { to {} }',
},
{
code: '@keyframes foo { from {} to {} }',
},
],

reject: [
{
code: '@keyframes foo { 0% {} }',
fixed: '@keyframes foo { from {} }',
message: messages.expected('0%', 'from'),
line: 1,
column: 18,
endLine: 1,
endColumn: 20,
},
{
code: '@keyframes foo { 100% {} }',
fixed: '@keyframes foo { to {} }',
message: messages.expected('100%', 'to'),
line: 1,
column: 18,
endLine: 1,
endColumn: 22,
},
{
code: '@keyframes foo { 0% {} 100% {} }',
fixed: '@keyframes foo { from {} to {} }',
warnings: [
{
message: messages.expected('0%', 'from'),
line: 1,
column: 18,
endLine: 1,
endColumn: 20,
},
{
message: messages.expected('100%', 'to'),
line: 1,
column: 24,
endLine: 1,
endColumn: 28,
},
],
},
],
});

testRule({
ruleName,
config: ['percentage'],
fix: true,

accept: [
{
code: '@keyframes foo { 0% {} }',
},
{
code: '@keyframes foo { 100% {} }',
},
{
code: '@keyframes foo { 0% {} 100% {} }',
},
],

reject: [
{
code: '@keyframes foo { from {} }',
fixed: '@keyframes foo { 0% {} }',
message: messages.expected('from', '0%'),
line: 1,
column: 18,
endLine: 1,
endColumn: 22,
},
{
code: '@keyframes foo { to {} }',
fixed: '@keyframes foo { 100% {} }',
message: messages.expected('to', '100%'),
line: 1,
column: 18,
endLine: 1,
endColumn: 20,
},
{
code: '@keyframes foo { from {} to {} }',
fixed: '@keyframes foo { 0% {} 100% {} }',
warnings: [
{
message: messages.expected('from', '0%'),
line: 1,
column: 18,
endLine: 1,
endColumn: 22,
},
{
message: messages.expected('to', '100%'),
line: 1,
column: 26,
endLine: 1,
endColumn: 28,
},
],
},
],
});
89 changes: 89 additions & 0 deletions lib/rules/keyframe-selector-notation/index.js
@@ -0,0 +1,89 @@
'use strict';

const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');

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/list/keyframe-selector-notation',
};

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

/** @type {import('stylelint').Rule<'keyword' | 'percentage'>} */
const rule = (primary, _, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['keyword', 'percentage'],
});

if (!validOptions) return;

/**
* @type {Record<primary, {
* expFunc: (selector: string) => boolean,
* fixFunc: (selector: string) => string,
* }>}
*/
const optionFuncs = Object.freeze({
keyword: {
expFunc: (selector) => KEYWORD_SELECTORS.has(selector),
fixFunc: (selector) => {
return selector === '0%' ? 'from' : 'to';
},
},
percentage: {
expFunc: (selector) => PERCENTAGE_SELECTORS.has(selector),
fixFunc: (selector) => {
return selector === 'from' ? '0%' : '100%';
},
},
});
mattxwang marked this conversation as resolved.
Show resolved Hide resolved

root.walkAtRules(/^(-(moz|webkit)-)?keyframes$/i, (atRuleKeyframes) => {
atRuleKeyframes.walkRules((keyframeRule) => {
const selector = keyframeRule.selector;

const normalizedSelector = selector.toLowerCase();

if (
!KEYWORD_SELECTORS.has(normalizedSelector) &&
!PERCENTAGE_SELECTORS.has(normalizedSelector)
) {
return;
}

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

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

if (context.fix) {
keyframeRule.selector = fixedSelector;

return;
}

report({
message: messages.expected(selector, fixedSelector),
node: keyframeRule,
result,
ruleName,
word: selector,
});
});
});
};
};

rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;