Skip to content

Commit 277d647

Browse files
immitsuMouvediajeddy3
authored
Add except: ["exact-value"] to media-feature-range-notation (#8675)
Co-authored-by: Gary Gozlan <Mouvedia@users.noreply.github.com> Co-authored-by: Richard Hallows <jeddy3@users.noreply.github.com>
1 parent 8f8b864 commit 277d647

File tree

6 files changed

+250
-21
lines changed

6 files changed

+250
-21
lines changed

.changeset/sour-fishes-fetch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"stylelint": minor
3+
---
4+
5+
Added: `except: ["exact-value"]` to `media-feature-range-notation`

lib/rules/media-feature-range-notation/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,37 @@ The following patterns are _not_ considered problems:
8484
```css
8585
@media (min-width: 1px) and (max-width: 2px) {}
8686
```
87+
88+
## Optional secondary options
89+
90+
### `except`
91+
92+
```json
93+
{ "except": ["array", "of", "options"] }
94+
```
95+
96+
#### `"exact-value"`
97+
98+
Reverse the primary option for media features with exact values.
99+
100+
Given:
101+
102+
```json
103+
{
104+
"media-feature-range-notation": ["context", { "except": ["exact-value"] }]
105+
}
106+
```
107+
108+
The following pattern is considered a problem:
109+
110+
<!-- prettier-ignore -->
111+
```css
112+
@media (min-width: 1px) {}
113+
```
114+
115+
The following pattern is _not_ considered a problem:
116+
117+
<!-- prettier-ignore -->
118+
```css
119+
@media (width: 1px) {}
120+
```

lib/rules/media-feature-range-notation/__tests__/index.mjs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,86 @@ testRule({
151151
],
152152
});
153153

154+
testRule({
155+
ruleName,
156+
config: ['context', { except: ['exact-value'] }],
157+
fix: true,
158+
computeEditInfo: true,
159+
160+
accept: [
161+
{
162+
code: '@media (width: 1px) {}',
163+
},
164+
{
165+
code: '@media (1px: width) {}',
166+
},
167+
{
168+
code: '@media (width >= 1px) {}',
169+
},
170+
{
171+
code: '@media (1px <= width <= 2px) {}',
172+
},
173+
],
174+
reject: [
175+
{
176+
unfixable: true,
177+
code: '@media (width = 1px) {}',
178+
message: messages.expected('prefix'),
179+
},
180+
{
181+
unfixable: true,
182+
code: '@media (1px = width) {}',
183+
message: messages.expected('prefix'),
184+
},
185+
{
186+
code: '@media (min-width: 1px) {}',
187+
fixed: '@media (width >= 1px) {}',
188+
fix: {
189+
range: [8, 18],
190+
text: 'width >=',
191+
},
192+
message: messages.expected('context'),
193+
column: 8,
194+
endColumn: 24,
195+
endLine: 1,
196+
line: 1,
197+
},
198+
{
199+
code: stripIndent`
200+
@media (min-width: 1px)
201+
and (max-width: 2px)
202+
and (width: 3px) {}
203+
`,
204+
fixed: stripIndent`
205+
@media (width >= 1px)
206+
and (width <= 2px)
207+
and (width: 3px) {}
208+
`,
209+
warnings: [
210+
{
211+
message: messages.expected('context'),
212+
line: 1,
213+
column: 8,
214+
endLine: 1,
215+
endColumn: 24,
216+
fix: {
217+
range: [8, 18],
218+
text: 'width >=',
219+
},
220+
},
221+
{
222+
message: messages.expected('context'),
223+
line: 2,
224+
column: 6,
225+
endLine: 2,
226+
endColumn: 22,
227+
fix: undefined,
228+
},
229+
],
230+
},
231+
],
232+
});
233+
154234
testRule({
155235
ruleName,
156236
config: ['prefix'],
@@ -233,3 +313,52 @@ testRule({
233313
},
234314
],
235315
});
316+
317+
testRule({
318+
ruleName,
319+
config: ['prefix', { except: ['exact-value'] }],
320+
fix: true,
321+
computeEditInfo: true,
322+
323+
accept: [
324+
{
325+
code: '@media (width = 1px) {}',
326+
},
327+
{
328+
code: '@media (1px = width) {}',
329+
},
330+
{
331+
code: '@media (min-width: 1px) {}',
332+
},
333+
{
334+
code: '@media (pointer: fine) {}',
335+
},
336+
{
337+
code: '@media (color) {}',
338+
},
339+
],
340+
reject: [
341+
{
342+
code: '@media (width > 1px) {}',
343+
unfixable: true,
344+
message: messages.expected('prefix'),
345+
line: 1,
346+
column: 8,
347+
endLine: 1,
348+
endColumn: 21,
349+
},
350+
{
351+
code: '@media (width: 1px) {}',
352+
fixed: '@media (width = 1px) {}',
353+
fix: {
354+
range: [13, 14],
355+
text: ' =',
356+
},
357+
message: messages.expected('context'),
358+
line: 1,
359+
column: 8,
360+
endLine: 1,
361+
endColumn: 20,
362+
},
363+
],
364+
});

lib/rules/media-feature-range-notation/index.cjs

Lines changed: 38 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/rules/media-feature-range-notation/index.mjs

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ import {
44
isMediaFeature,
55
isMediaFeaturePlain,
66
isMediaFeatureRange,
7+
isMediaFeatureRangeNameValue,
8+
isMediaFeatureRangeValueName,
79
isMediaQueryInvalid,
810
} from '@csstools/media-query-list-parser';
911
import { TokenNode, sourceIndices } from '@csstools/css-parser-algorithms';
1012
import { TokenType } from '@csstools/css-tokenizer';
1113

1214
import { atRuleParamIndex } from '../../utils/nodeFieldIndices.mjs';
1315
import { atRuleRegexes } from '../../utils/regexes.mjs';
16+
import optionsMatches from '../../utils/optionsMatches.mjs';
1417
import parseMediaQuery from '../../utils/parseMediaQuery.mjs';
1518
import { rangeTypeMediaFeatureNames } from '../../reference/mediaFeatures.mjs';
1619
import report from '../../utils/report.mjs';
@@ -31,17 +34,30 @@ const meta = {
3134
/** @import {TokenDelim} from '@csstools/css-tokenizer' */
3235

3336
/** @type {import('stylelint').CoreRules[ruleName]} */
34-
const rule = (primary) => {
37+
const rule = (primary, secondaryOptions) => {
3538
return (root, result) => {
36-
const validOptions = validateOptions(result, ruleName, {
37-
actual: primary,
38-
possible: ['prefix', 'context'],
39-
});
39+
const validOptions = validateOptions(
40+
result,
41+
ruleName,
42+
{
43+
actual: primary,
44+
possible: ['prefix', 'context'],
45+
},
46+
{
47+
actual: secondaryOptions,
48+
possible: {
49+
except: ['exact-value'],
50+
},
51+
optional: true,
52+
},
53+
);
4054

4155
if (!validOptions) {
4256
return;
4357
}
4458

59+
const exceptExactValue = optionsMatches(secondaryOptions, 'except', 'exact-value');
60+
4561
root.walkAtRules(atRuleRegexes.mediaName, (atRule) => {
4662
const mediaQueryList = parseMediaQuery(atRule);
4763

@@ -52,17 +68,31 @@ const rule = (primary) => {
5268
// Only look at plain and range notation media features
5369
if (!isMediaFeatureRange(node) && !isMediaFeaturePlain(node)) return;
5470

71+
const featureName = node.getName();
72+
const unprefixedMediaFeature = featureName.replace(/^(?:min|max)-/i, '');
73+
74+
if (!rangeTypeMediaFeatureNames.has(unprefixedMediaFeature)) return;
75+
76+
if (exceptExactValue) {
77+
const isMediaFeatureRangeWithExactOperator =
78+
(isMediaFeatureRangeNameValue(node) || isMediaFeatureRangeValueName(node)) &&
79+
node.operator.length === 1 &&
80+
node.operator[0][1] === '=';
81+
82+
const isMediaFeaturePlainUnprefixed =
83+
isMediaFeaturePlain(node) && featureName.length === unprefixedMediaFeature.length;
84+
85+
if (isMediaFeatureRangeWithExactOperator || isMediaFeaturePlainUnprefixed) {
86+
primary = primary === 'prefix' ? 'context' : 'prefix';
87+
}
88+
}
89+
5590
// Expected plain notation and received plain notation
5691
if (primary === 'prefix' && isMediaFeaturePlain(node)) return;
5792

5893
// Expected range notation and received range notation
5994
if (primary === 'context' && isMediaFeatureRange(node)) return;
6095

61-
const featureName = node.getName();
62-
const unprefixedMediaFeature = featureName.replace(/^(?:min|max)-/i, '');
63-
64-
if (!rangeTypeMediaFeatureNames.has(unprefixedMediaFeature)) return;
65-
6696
/**
6797
* @param {object} entry
6898
* @param {import('@csstools/media-query-list-parser').MediaFeaturePlain} entry.node

types/stylelint/index.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,10 @@ declare namespace stylelint {
760760
{},
761761
RejectedMessage<[name: string, value: string]>
762762
>;
763-
'media-feature-range-notation': NotationRule<'prefix' | 'context'>;
763+
'media-feature-range-notation': NotationRule<
764+
'prefix' | 'context',
765+
{ except: OneOrMany<'exact-value'> }
766+
>;
764767
'media-query-no-invalid': CoreRule<
765768
true,
766769
{ ignoreFunctions: OneOrMany<StringOrRegex> },

0 commit comments

Comments
 (0)