Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
media-feature-name-unit-allowed-list
(#6550)
* First pass: `media-feature-name-unit-allowed-list` Adds base rule, refactors slightly from @romainmenke's suggestion; adds docs, rudimentary tests. Co-authored-by: Romain Menke <11521496+romainmenke@users.noreply.github.com> * Fix end positions Co-authored-by: Romain Menke <11521496+romainmenke@users.noreply.github.com> * add tests for feature names with no attached unit * Properly grabs `name` for range queries Required a bit of hardcoding? * add changeset * Uses `entry.node.name.getName()` Co-authored-by: Romain Menke <11521496+romainmenke@users.noreply.github.com> * Update changeset to match naming convention * Update changeset to match naming convention (again) Co-authored-by: Marc G. <Mouvedia@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Masafumi Koba <473530+ybiquitous@users.noreply.github.com> * misc standardization * fix postcss location * Improve readability/conciseness of library code No behaviour changes. Co-authored-by: Masafumi Koba <473530+ybiquitous@users.noreply.github.com> Co-authored-by: Romain Menke <11521496+romainmenke@users.noreply.github.com> Co-authored-by: Marc G. <Mouvedia@users.noreply.github.com> Co-authored-by: Masafumi Koba <473530+ybiquitous@users.noreply.github.com>
- Loading branch information
1 parent
f5b27ca
commit 00de033
Showing
8 changed files
with
416 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"stylelint": minor | ||
--- | ||
|
||
Added: `media-feature-name-unit-allowed-list` rule |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
# media-feature-name-unit-allowed-list | ||
|
||
Specify a list of allowed name and unit pairs within media features. | ||
|
||
<!-- prettier-ignore --> | ||
```css | ||
@media (width: 50em) {} | ||
/** ↑ ↑ | ||
* This media feature name and these units */ | ||
``` | ||
|
||
## Options | ||
|
||
`object`: `{ "name": ["array", "of", "units"]|"unit" }` | ||
|
||
If a feature name is surrounded with `"/"` (e.g. `"/height/"`), it is interpreted as a regular expression. This allows, for example, easy targeting of shorthands: `/height/` will match `height`, `min-height`, `max-height`, etc. | ||
|
||
Given: | ||
|
||
```json | ||
{ | ||
"width": "em", | ||
"/height/": ["em", "rem"] | ||
} | ||
``` | ||
|
||
The following patterns are considered problems: | ||
|
||
<!-- prettier-ignore --> | ||
```css | ||
@media (width: 50rem) {} | ||
``` | ||
|
||
<!-- prettier-ignore --> | ||
```css | ||
@media (height: 1000px) {} | ||
``` | ||
|
||
<!-- prettier-ignore --> | ||
```css | ||
@media (min-height: 1000px) {} | ||
``` | ||
|
||
<!-- prettier-ignore --> | ||
```css | ||
@media (height <= 1000px) {} | ||
``` | ||
|
||
The following patterns are _not_ considered problems: | ||
|
||
<!-- prettier-ignore --> | ||
```css | ||
@media (width: 50em) {} | ||
``` | ||
|
||
<!-- prettier-ignore --> | ||
```css | ||
@media (width <= 50em) {} | ||
``` | ||
|
||
<!-- prettier-ignore --> | ||
```css | ||
@media (height: 50em) {} | ||
``` | ||
|
||
<!-- prettier-ignore --> | ||
```css | ||
@media (min-height: 50rem) {} | ||
``` |
170 changes: 170 additions & 0 deletions
170
lib/rules/media-feature-name-unit-allowed-list/__tests__/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
'use strict'; | ||
|
||
const { messages, ruleName } = require('..'); | ||
|
||
testRule({ | ||
ruleName, | ||
config: { width: 'em', '/height/': ['em', 'rem'] }, | ||
|
||
accept: [ | ||
{ | ||
code: '@media (width: 50em) {}', | ||
}, | ||
{ | ||
code: '@media (width <= 50em) {}', | ||
description: 'media queries level 4 - inequality', | ||
}, | ||
{ | ||
code: '@media (min-width: 50rem) {}', | ||
}, | ||
{ | ||
code: '@media (max-width: 50rem) {}', | ||
}, | ||
{ | ||
code: '@media (30em <= width <= 50em) {}', | ||
description: 'media queries level 4 - range', | ||
}, | ||
{ | ||
code: '@media print and (width: 50em) {}', | ||
description: 'compound selector', | ||
}, | ||
{ | ||
code: '@media (height: 50rem) {}', | ||
}, | ||
{ | ||
code: '@media (min-height: 50rem) {}', | ||
}, | ||
{ | ||
code: '@media (max-height: 50rem) {}', | ||
}, | ||
{ | ||
code: '@media (30rem <= height <= 50rem) {}', | ||
description: 'media queries level 4 - range', | ||
}, | ||
], | ||
|
||
reject: [ | ||
{ | ||
code: '@media (width: 50rem) {}', | ||
message: messages.rejected('rem', 'width'), | ||
line: 1, | ||
column: 16, | ||
endLine: 1, | ||
endColumn: 21, | ||
}, | ||
{ | ||
code: '@media (width <= 50rem) {}', | ||
message: messages.rejected('rem', 'width'), | ||
description: 'media queries level 4 - inequality', | ||
line: 1, | ||
column: 18, | ||
endLine: 1, | ||
endColumn: 23, | ||
}, | ||
{ | ||
code: '@media (30rem <= width <= 50rem) {}', | ||
description: 'media queries level 4 - range', | ||
warnings: [ | ||
{ | ||
message: messages.rejected('rem', 'width'), | ||
line: 1, | ||
column: 9, | ||
endLine: 1, | ||
endColumn: 14, | ||
}, | ||
{ | ||
message: messages.rejected('rem', 'width'), | ||
line: 1, | ||
column: 27, | ||
endLine: 1, | ||
endColumn: 32, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: '@media (width: 1000px) {}', | ||
message: messages.rejected('px', 'width'), | ||
line: 1, | ||
column: 16, | ||
endLine: 1, | ||
endColumn: 22, | ||
}, | ||
{ | ||
code: '@media (height: 1000px) {}', | ||
message: messages.rejected('px', 'height'), | ||
line: 1, | ||
column: 17, | ||
endLine: 1, | ||
endColumn: 23, | ||
}, | ||
{ | ||
code: '@media (height <= 1000px) {}', | ||
message: messages.rejected('px', 'height'), | ||
description: 'media queries level 4 - inequality', | ||
line: 1, | ||
column: 19, | ||
endLine: 1, | ||
endColumn: 25, | ||
}, | ||
{ | ||
code: '@media (100px <= height <= 1000px) {}', | ||
description: 'media queries level 4 - range', | ||
warnings: [ | ||
{ | ||
message: messages.rejected('px', 'height'), | ||
line: 1, | ||
column: 9, | ||
endLine: 1, | ||
endColumn: 14, | ||
}, | ||
{ | ||
message: messages.rejected('px', 'height'), | ||
line: 1, | ||
column: 28, | ||
endLine: 1, | ||
endColumn: 34, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: '@media (min-height: 1000px) {}', | ||
message: messages.rejected('px', 'min-height'), | ||
line: 1, | ||
column: 21, | ||
endLine: 1, | ||
endColumn: 27, | ||
}, | ||
{ | ||
code: '@media (max-height: 1000px) {}', | ||
message: messages.rejected('px', 'max-height'), | ||
line: 1, | ||
column: 21, | ||
endLine: 1, | ||
endColumn: 27, | ||
}, | ||
], | ||
}); | ||
|
||
testRule({ | ||
ruleName, | ||
config: { grid: [], 'prefers-reduced-motion': [], 'video-dynamic-range': [] }, | ||
|
||
accept: [ | ||
{ | ||
code: '@media (prefers-reduced-motion) {}', | ||
description: 'name-only value', | ||
}, | ||
{ | ||
code: '@media (color) {}', | ||
description: 'name-only value', | ||
}, | ||
{ | ||
code: '@media (grid: 0) {}', | ||
description: 'unitless value, number', | ||
}, | ||
{ | ||
code: '@media (video-dynamic-range: high) {}', | ||
description: 'unitless value, keyword', | ||
}, | ||
], | ||
}); |
102 changes: 102 additions & 0 deletions
102
lib/rules/media-feature-name-unit-allowed-list/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
'use strict'; | ||
|
||
const report = require('../../utils/report'); | ||
const ruleMessages = require('../../utils/ruleMessages'); | ||
const validateOptions = require('../../utils/validateOptions'); | ||
const { TokenType } = require('@csstools/css-tokenizer'); | ||
const { isTokenNode } = require('@csstools/css-parser-algorithms'); | ||
const { | ||
isMediaFeaturePlain, | ||
isMediaFeatureRange, | ||
parse, | ||
} = require('@csstools/media-query-list-parser'); | ||
const validateObjectWithArrayProps = require('../../utils/validateObjectWithArrayProps'); | ||
const { isString } = require('../../utils/validateTypes'); | ||
const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); | ||
const atRuleParamIndex = require('../../utils/atRuleParamIndex'); | ||
|
||
const ruleName = 'media-feature-name-unit-allowed-list'; | ||
|
||
const messages = ruleMessages(ruleName, { | ||
rejected: (unit, name) => `Unexpected unit "${unit}" for name "${name}"`, | ||
}); | ||
|
||
const meta = { | ||
url: 'https://stylelint.io/user-guide/rules/media-feature-name-unit-allowed-list', | ||
}; | ||
|
||
/** @type {import('stylelint').Rule<Record<string, string | string[]>>} */ | ||
const rule = (primary) => { | ||
return (root, result) => { | ||
const validOptions = validateOptions(result, ruleName, { | ||
actual: primary, | ||
possible: [validateObjectWithArrayProps(isString)], | ||
}); | ||
|
||
if (!validOptions) { | ||
return; | ||
} | ||
|
||
const primaryPairs = Object.entries(primary); | ||
const primaryUnitList = (/** @type {string} */ featureName) => { | ||
for (const [name, unit] of primaryPairs) { | ||
if (matchesStringOrRegExp(featureName, name)) return [unit].flat(); | ||
} | ||
|
||
return undefined; | ||
}; | ||
|
||
root.walkAtRules(/^media$/i, (atRule) => { | ||
const mediaQueryList = parse(atRule.params); | ||
|
||
mediaQueryList.forEach((mediaQuery) => { | ||
mediaQuery.walk((entry) => { | ||
if (!isMediaFeaturePlain(entry.node) && !isMediaFeatureRange(entry.node)) { | ||
return; | ||
} | ||
|
||
const featureName = entry.node.name.getName(); | ||
const unitList = primaryUnitList(featureName); | ||
|
||
if (!unitList) { | ||
return; | ||
} | ||
|
||
entry.node.walk(({ node: childNode }) => { | ||
if (!isTokenNode(childNode)) { | ||
return; | ||
} | ||
|
||
const [tokenType, , startIndex, endIndex] = childNode.value; | ||
|
||
if (tokenType !== TokenType.Dimension) { | ||
return; | ||
} | ||
|
||
const unit = childNode.value[4].unit; | ||
|
||
if (unitList.includes(unit.toLowerCase())) { | ||
return; | ||
} | ||
|
||
const atRuleIndex = atRuleParamIndex(atRule); | ||
|
||
report({ | ||
message: messages.rejected(unit, featureName), | ||
node: atRule, | ||
index: atRuleIndex + startIndex, | ||
endIndex: atRuleIndex + endIndex + 1, | ||
result, | ||
ruleName, | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}; | ||
}; | ||
|
||
rule.ruleName = ruleName; | ||
rule.messages = messages; | ||
rule.meta = meta; | ||
module.exports = rule; |
Oops, something went wrong.