diff --git a/lib/rules/media-feature-name-blacklist/README.md b/lib/rules/media-feature-name-blacklist/README.md index b7103e9436..f551883512 100644 --- a/lib/rules/media-feature-name-blacklist/README.md +++ b/lib/rules/media-feature-name-blacklist/README.md @@ -8,8 +8,6 @@ Specify a blacklist of disallowed media feature names. * This media feature name */ ``` -**Caveat:** Media feature names within a range context are currently ignored. - ## Options `array|string|regex`: `["array", "of", "unprefixed", /media-features/ or "regex"]|"media-feature"|/regex/` @@ -30,6 +28,14 @@ The following patterns are considered violations: @media (my-width: 50em) {} ``` +```css +@media (max-width < 50em) {} +``` + +```css +@media (10em < my-height < 50em) {} +``` + The following patterns are *not* considered violations: ```css @@ -39,3 +45,11 @@ The following patterns are *not* considered violations: ```css @media print and (min-resolution: 300dpi) {} ``` + +```css +@media (min-width >= 50em) {} +``` + +```css +@media (10em < width < 50em) {} +``` diff --git a/lib/rules/media-feature-name-blacklist/__tests__/index.js b/lib/rules/media-feature-name-blacklist/__tests__/index.js index 8ec59fcea5..70d8395960 100644 --- a/lib/rules/media-feature-name-blacklist/__tests__/index.js +++ b/lib/rules/media-feature-name-blacklist/__tests__/index.js @@ -18,20 +18,18 @@ testRule(rule, { code: '@media (MiN-wIdTh: 50em) { }', }, { - code: '@media (--wide-viewport) { }', - description: 'ignore custom media query', + code: '@media (height <= 50em) { }', }, { - code: '@media (/* max-width: 50em */ min-width: 50em) { }', - description: 'ignore comments', + code: '@media (400px < height < 1000px) { }', }, { - code: '@media (width <= 50em) { }', - description: 'ignore media features in a range context', + code: '@media (--wide-viewport) { }', + description: 'ignore custom media query', }, { - code: '@media (400px < width < 1000px) { }', - description: 'ignore media features in a range context', + code: '@media (/* max-width: 50em */ min-width: 50em) { }', + description: 'ignore comments', }, { code: '@media (monochrome) { }', @@ -70,6 +68,33 @@ testRule(rule, { line: 1, column: 9, }, + { + code: '@media (width: 50em) { }', + message: messages.rejected('width'), + line: 1, + column: 9, + }, + { + code: '@media (20em < width <= 50em) { }', + message: messages.rejected('width'), + line: 1, + column: 16, + }, + { + code: '@media (10em < max-width <= 50em) and (width > 50em) { }', + warnings: [ + { + message: messages.rejected('max-width'), + line: 1, + column: 16, + }, + { + message: messages.rejected('width'), + line: 1, + column: 40, + }, + ], + }, ], }); @@ -90,6 +115,24 @@ testRule(rule, { line: 1, column: 9, }, + { + code: '@media (my-width >= 50em) { }', + message: messages.rejected('my-width'), + line: 1, + column: 9, + }, + { + code: '@media (10em < my-width <= 50em) { }', + message: messages.rejected('my-width'), + line: 1, + column: 16, + }, + { + code: '@media (50em < my-width) { }', + message: messages.rejected('my-width'), + line: 1, + column: 16, + }, ], }); diff --git a/lib/rules/media-feature-name-blacklist/index.js b/lib/rules/media-feature-name-blacklist/index.js index 9c610c0a7e..7de650c4c2 100644 --- a/lib/rules/media-feature-name-blacklist/index.js +++ b/lib/rules/media-feature-name-blacklist/index.js @@ -7,6 +7,7 @@ const isRangeContextMediaFeature = require('../../utils/isRangeContextMediaFeatu const isStandardSyntaxMediaFeatureName = require('../../utils/isStandardSyntaxMediaFeatureName'); const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); const mediaParser = require('postcss-media-query-parser').default; +const rangeContextNodeParser = require('../rangeContextNodeParser'); const report = require('../../utils/report'); const ruleMessages = require('../../utils/ruleMessages'); const validateOptions = require('../../utils/validateOptions'); @@ -31,14 +32,22 @@ function rule(blacklist) { root.walkAtRules(/^media$/i, (atRule) => { mediaParser(atRule.params).walk(/^media-feature$/i, (mediaFeatureNode) => { const parent = mediaFeatureNode.parent; - const sourceIndex = mediaFeatureNode.sourceIndex; - const value = mediaFeatureNode.value; + const mediaFeatureRangeContext = isRangeContextMediaFeature(parent.value); - if ( - isRangeContextMediaFeature(parent.value) || - !isStandardSyntaxMediaFeatureName(value) || - isCustomMediaQuery(value) - ) { + let value; + let sourceIndex; + + if (mediaFeatureRangeContext) { + const parsedRangeContext = rangeContextNodeParser(mediaFeatureNode); + + value = parsedRangeContext.name.value; + sourceIndex = parsedRangeContext.name.sourceIndex; + } else { + value = mediaFeatureNode.value; + sourceIndex = mediaFeatureNode.sourceIndex; + } + + if (!isStandardSyntaxMediaFeatureName(value) || isCustomMediaQuery(value)) { return; } diff --git a/lib/rules/media-feature-name-case/README.md b/lib/rules/media-feature-name-case/README.md index 7cefa97661..6e0e4ea4ad 100644 --- a/lib/rules/media-feature-name-case/README.md +++ b/lib/rules/media-feature-name-case/README.md @@ -8,8 +8,6 @@ Specify lowercase or uppercase for media feature names. * This media feature name */ ``` -This rule ignores media feature names within a range context. - The [`fix` option](../../../docs/user-guide/usage/options.md#fix) can automatically fix all of the problems reported by this rule. ## Options @@ -32,6 +30,10 @@ The following patterns are considered violations: @media (min-width: 700px) and (ORIENTATION: landscape) {} ``` +```css +@media (WIDTH > 10em) {} +``` + The following patterns are *not* considered violations: ```css @@ -46,6 +48,10 @@ The following patterns are *not* considered violations: @media (min-width: 700px) and (orientation: landscape) {} ``` +```css +@media (width > 10em) {} +``` + ### `"upper"` The following patterns are considered violations: @@ -62,6 +68,10 @@ The following patterns are considered violations: @media (MIN-WIDTH: 700px) and (orientation: landscape) {} ``` +```css +@media (10em < width <= 50em) {} +``` + The following patterns are *not* considered violations: ```css @@ -75,3 +85,7 @@ The following patterns are *not* considered violations: ```css @media (MIN-WIDTH: 700px) and (ORIENTATION: landscape) {} ``` + +```css +@media (10em < WIDTH <= 50em) {} +``` diff --git a/lib/rules/media-feature-name-case/__tests__/index.js b/lib/rules/media-feature-name-case/__tests__/index.js index 2828ccd570..cffe3b9d0f 100644 --- a/lib/rules/media-feature-name-case/__tests__/index.js +++ b/lib/rules/media-feature-name-case/__tests__/index.js @@ -18,6 +18,18 @@ testRule(rule, { { code: '@media (min-width: 700PX) { }', }, + { + code: '@media (width < 100px) { }', + }, + { + code: '@media (width = 100px) { }', + }, + { + code: '@media (width <= 100px) { }', + }, + { + code: '@media (10px <= width <= 100px) { }', + }, { code: '@media (min-width: 700px) and (orientation: landscape) { }', }, @@ -48,22 +60,6 @@ testRule(rule, { code: '@media (--VIEWPORT-MEDIUM) { }', description: 'ignore css variables', }, - { - code: '@media (WIDTH < 100px) { }', - description: 'ignore range context', - }, - { - code: '@media (WIDTH = 100px) { }', - description: 'ignore range context', - }, - { - code: '@media (WIDTH <= 100px) { }', - description: 'ignore range context', - }, - { - code: '@media (10px >= WIDTH <= 100px) { }', - description: 'ignore complex range context', - }, ], reject: [ @@ -133,6 +129,41 @@ testRule(rule, { line: 1, column: 9, }, + { + code: '@media (height: 50em) and (orientation: landscape) and (WIDTH: 25em) {}', + fixed: '@media (height: 50em) and (orientation: landscape) and (width: 25em) {}', + message: messages.expected('WIDTH', 'width'), + line: 1, + column: 57, + }, + { + code: '@media (WIDTH > 50em) {}', + fixed: '@media (width > 50em) {}', + message: messages.expected('WIDTH', 'width'), + line: 1, + column: 9, + }, + { + code: '@media (10em < WIDTH <= 50em) {}', + fixed: '@media (10em < width <= 50em) {}', + message: messages.expected('WIDTH', 'width'), + line: 1, + column: 16, + }, + { + code: '@media (width > 10em) and (WIDTH < 50em) {}', + fixed: '@media (width > 10em) and (width < 50em) {}', + message: messages.expected('WIDTH', 'width'), + line: 1, + column: 28, + }, + { + code: '@media (10em < WIDTH) {}', + fixed: '@media (10em < width) {}', + message: messages.expected('WIDTH', 'width'), + line: 1, + column: 16, + }, ], }); diff --git a/lib/rules/media-feature-name-case/index.js b/lib/rules/media-feature-name-case/index.js index 2ad19e7283..249b82ac62 100644 --- a/lib/rules/media-feature-name-case/index.js +++ b/lib/rules/media-feature-name-case/index.js @@ -6,6 +6,7 @@ const isCustomMediaQuery = require('../../utils/isCustomMediaQuery'); const isRangeContextMediaFeature = require('../../utils/isRangeContextMediaFeature'); const isStandardSyntaxMediaFeatureName = require('../../utils/isStandardSyntaxMediaFeatureName'); const mediaParser = require('postcss-media-query-parser').default; +const rangeContextNodeParser = require('../rangeContextNodeParser'); const report = require('../../utils/report'); const ruleMessages = require('../../utils/ruleMessages'); const validateOptions = require('../../utils/validateOptions'); @@ -33,14 +34,22 @@ function rule(expectation, options, context) { mediaParser(mediaRule).walk(/^media-feature$/i, (mediaFeatureNode) => { const parent = mediaFeatureNode.parent; - const sourceIndex = mediaFeatureNode.sourceIndex; - const value = mediaFeatureNode.value; - - if ( - isRangeContextMediaFeature(parent.value) || - !isStandardSyntaxMediaFeatureName(value) || - isCustomMediaQuery(value) - ) { + const mediaFeatureRangeContext = isRangeContextMediaFeature(parent.value); + + let value; + let sourceIndex; + + if (mediaFeatureRangeContext) { + const parsedRangeContext = rangeContextNodeParser(mediaFeatureNode); + + value = parsedRangeContext.name.value; + sourceIndex = parsedRangeContext.name.sourceIndex; + } else { + value = mediaFeatureNode.value; + sourceIndex = mediaFeatureNode.sourceIndex; + } + + if (!isStandardSyntaxMediaFeatureName(value) || isCustomMediaQuery(value)) { return; } diff --git a/lib/rules/media-feature-name-no-unknown/README.md b/lib/rules/media-feature-name-no-unknown/README.md index b686258d46..71b2f918b3 100644 --- a/lib/rules/media-feature-name-no-unknown/README.md +++ b/lib/rules/media-feature-name-no-unknown/README.md @@ -10,10 +10,7 @@ Disallow unknown media feature names. This rule considers media feature names defined in the CSS Specifications, up to and including Editor's Drafts, to be known. -This rule ignores: - -- media feature names within a range context -- vendor-prefixed media feature names +This rule ignores vendor-prefixed media feature names. ## Options @@ -29,6 +26,10 @@ The following patterns are considered violations: @media screen and (unknown: 10px) {} ``` +```css +@media screen and (unknown > 10px) {} +``` + The following patterns are *not* considered violations: ```css @@ -71,6 +72,10 @@ The following patterns are *not* considered violations: @media screen and (custom: 10px) {} ``` +```css +@media screen and (100px < custom < 700px) {} +``` + ```css @media (min-width: 700px) and (custom: 10px) {} ``` diff --git a/lib/rules/media-feature-name-no-unknown/__tests__/index.js b/lib/rules/media-feature-name-no-unknown/__tests__/index.js index 47216ae533..e194b952ea 100644 --- a/lib/rules/media-feature-name-no-unknown/__tests__/index.js +++ b/lib/rules/media-feature-name-no-unknown/__tests__/index.js @@ -21,33 +21,29 @@ testRule(rule, { code: '@media (MIN-WIDTH: 700px) { }', }, { - code: '@media (min-width: 700px) and (orientation: landscape) { }', + code: '@media (width < 100px) { }', }, { - code: '@media (MIN-WIDTH: 700px) and (ORIENTATION: landscape) { }', + code: '@media (width = 100px) { }', }, { - code: '@media (-webkit-min-device-pixel-ratio: 2) { }', + code: '@media (width <= 100px) { }', }, { - code: '@media (--viewport-medium) { }', - description: 'ignore css variables', + code: '@media (10px >= width <= 100px) { }', }, { - code: '@media (width < 100px) { }', - description: 'ignore range context', + code: '@media (min-width: 700px) and (orientation: landscape) { }', }, { - code: '@media (width = 100px) { }', - description: 'ignore range context', + code: '@media (MIN-WIDTH: 700px) and (ORIENTATION: landscape) { }', }, { - code: '@media (width <= 100px) { }', - description: 'ignore range context', + code: '@media (-webkit-min-device-pixel-ratio: 2) { }', }, { - code: '@media (10px >= width <= 100px) { }', - description: 'ignore complex range context', + code: '@media (--viewport-medium) { }', + description: 'ignore css variables', }, ], @@ -88,6 +84,24 @@ testRule(rule, { line: 1, column: 32, }, + { + code: '@media (UNKNOWN >= 50em) { }', + message: messages.rejected('UNKNOWN'), + line: 1, + column: 9, + }, + { + code: '@media (10em < unknown <= 50em) { }', + message: messages.rejected('unknown'), + line: 1, + column: 16, + }, + { + code: '@media (10em < width < 50em) and (50em < unknown) { }', + message: messages.rejected('unknown'), + line: 1, + column: 42, + }, ], }); diff --git a/lib/rules/media-feature-name-no-unknown/index.js b/lib/rules/media-feature-name-no-unknown/index.js index 1f00658644..695aa82be4 100644 --- a/lib/rules/media-feature-name-no-unknown/index.js +++ b/lib/rules/media-feature-name-no-unknown/index.js @@ -9,6 +9,7 @@ const keywordSets = require('../../reference/keywordSets'); const mediaParser = require('postcss-media-query-parser').default; const optionsMatches = require('../../utils/optionsMatches'); const postcss = require('postcss'); +const rangeContextNodeParser = require('../rangeContextNodeParser'); const report = require('../../utils/report'); const ruleMessages = require('../../utils/ruleMessages'); const validateOptions = require('../../utils/validateOptions'); @@ -41,14 +42,22 @@ function rule(actual, options) { root.walkAtRules(/^media$/i, (atRule) => { mediaParser(atRule.params).walk(/^media-feature$/i, (mediaFeatureNode) => { const parent = mediaFeatureNode.parent; - const sourceIndex = mediaFeatureNode.sourceIndex; - const value = mediaFeatureNode.value; + const mediaFeatureRangeContext = isRangeContextMediaFeature(parent.value); - if ( - isRangeContextMediaFeature(parent.value) || - !isStandardSyntaxMediaFeatureName(value) || - isCustomMediaQuery(value) - ) { + let value; + let sourceIndex; + + if (mediaFeatureRangeContext) { + const parsedRangeContext = rangeContextNodeParser(mediaFeatureNode); + + value = parsedRangeContext.name.value; + sourceIndex = parsedRangeContext.name.sourceIndex; + } else { + value = mediaFeatureNode.value; + sourceIndex = mediaFeatureNode.sourceIndex; + } + + if (!isStandardSyntaxMediaFeatureName(value) || isCustomMediaQuery(value)) { return; } diff --git a/lib/rules/media-feature-name-no-vendor-prefix/__tests__/index.js b/lib/rules/media-feature-name-no-vendor-prefix/__tests__/index.js index 72fefefa1c..6acd9bcffb 100644 --- a/lib/rules/media-feature-name-no-vendor-prefix/__tests__/index.js +++ b/lib/rules/media-feature-name-no-vendor-prefix/__tests__/index.js @@ -47,5 +47,17 @@ testRule(rule, { line: 1, column: 11, }, + { + code: '@media (-o-max-device-pixel-ratio > 1) {}', + message: messages.rejected, + line: 1, + column: 9, + }, + { + code: '@media (1 < -o-max-device-pixel-ratio < 2) {}', + message: messages.rejected, + line: 1, + column: 13, + }, ], }); diff --git a/lib/rules/media-feature-name-value-whitelist/README.md b/lib/rules/media-feature-name-value-whitelist/README.md index 026c4d3935..2bd5b6aa63 100644 --- a/lib/rules/media-feature-name-value-whitelist/README.md +++ b/lib/rules/media-feature-name-value-whitelist/README.md @@ -8,8 +8,6 @@ Specify a whitelist of allowed media feature name and value pairs. * These features and values */ ``` -This rule ignores media features within range and boolean context. - ## Options ```js @@ -45,6 +43,10 @@ The following patterns are considered violations: @media screen and (min-resolution: 2dpi) {} ``` +```css +@media screen and (min-width > 1000px) {} +``` + The following patterns are *not* considered violations: ```css @@ -66,3 +68,7 @@ The following patterns are *not* considered violations: ```css @media screen and (resolution: 10dpcm) {} ``` + +```css +@media screen and (768px < min-width) {} +``` diff --git a/lib/rules/media-feature-name-value-whitelist/__tests__/index.js b/lib/rules/media-feature-name-value-whitelist/__tests__/index.js index 6ad029d57e..a7f650a835 100644 --- a/lib/rules/media-feature-name-value-whitelist/__tests__/index.js +++ b/lib/rules/media-feature-name-value-whitelist/__tests__/index.js @@ -52,13 +52,9 @@ testRule(rule, { description: 'Boolean context with colon in comments', }, { - code: '@media screen and (width <= 768px) {}', + code: '@media screen and (min-width <= 768px) {}', description: 'Range context, media feature in whitelist', }, - { - code: '@media screen and (height <= 768px) {}', - description: 'Range context, media feature NOT in whitelist', - }, ], reject: [ @@ -102,6 +98,38 @@ testRule(rule, { line: 1, column: 37, }, + { + code: '@media screen and (min-width > 500px) {}', + message: messages.rejected('min-width', '500px'), + line: 1, + column: 32, + }, + { + code: '@media screen and (400px < min-width) {}', + message: messages.rejected('min-width', '400px'), + line: 1, + column: 20, + }, + { + code: '@media (400px < min-width < 500px) and (min-width < 1200px)', + warnings: [ + { + message: messages.rejected('min-width', '400px'), + line: 1, + column: 9, + }, + { + message: messages.rejected('min-width', '500px'), + line: 1, + column: 29, + }, + { + message: messages.rejected('min-width', '1200px'), + line: 1, + column: 53, + }, + ], + }, ], }); @@ -135,5 +163,11 @@ testRule(rule, { line: 1, column: 37, }, + { + code: '@media screen and (min-resolution > 2dpi) {}', + message: messages.rejected('min-resolution', '2dpi'), + line: 1, + column: 38, + }, ], }); diff --git a/lib/rules/media-feature-name-value-whitelist/index.js b/lib/rules/media-feature-name-value-whitelist/index.js index bc110c6396..15cab7e33d 100644 --- a/lib/rules/media-feature-name-value-whitelist/index.js +++ b/lib/rules/media-feature-name-value-whitelist/index.js @@ -2,9 +2,11 @@ const _ = require('lodash'); const atRuleParamIndex = require('../../utils/atRuleParamIndex'); +const isRangeContextMediaFeature = require('../../utils/isRangeContextMediaFeature'); const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); const mediaParser = require('postcss-media-query-parser').default; const postcss = require('postcss'); +const rangeContextNodeParser = require('../rangeContextNodeParser'); const report = require('../../utils/report'); const ruleMessages = require('../../utils/ruleMessages'); const validateOptions = require('../../utils/validateOptions'); @@ -28,36 +30,53 @@ function rule(whitelist) { root.walkAtRules(/^media$/i, (atRule) => { mediaParser(atRule.params).walk(/^media-feature-expression$/i, (node) => { - // Ignore boolean and range context - if (!node.value.includes(':')) { + const mediaFeatureRangeContext = isRangeContextMediaFeature(node.parent.value); + + // Ignore boolean + if (!node.value.includes(':') && !mediaFeatureRangeContext) { return; } const mediaFeatureNode = _.find(node.nodes, { type: 'media-feature' }); - const valueNode = _.find(node.nodes, { type: 'value' }); - const mediaFeatureName = mediaFeatureNode.value; - const value = valueNode.value; - const unprefixedMediaFeatureName = postcss.vendor.unprefixed(mediaFeatureName); - const featureWhitelist = _.find(whitelist, (v, whitelistFeatureName) => - matchesStringOrRegExp(unprefixedMediaFeatureName, whitelistFeatureName), - ); + let mediaFeatureName; + let values = []; - if (featureWhitelist === undefined) { - return; - } + if (mediaFeatureRangeContext) { + const parsedRangeContext = rangeContextNodeParser(mediaFeatureNode); - if (matchesStringOrRegExp(value, featureWhitelist)) { - return; + mediaFeatureName = parsedRangeContext.name.value; + values = parsedRangeContext.values; + } else { + mediaFeatureName = mediaFeatureNode.value; + values.push(_.find(node.nodes, { type: 'value' })); } - report({ - index: atRuleParamIndex(atRule) + valueNode.sourceIndex, - message: messages.rejected(mediaFeatureName, value), - node: atRule, - ruleName, - result, - }); + for (let i = 0; i < values.length; i++) { + const valueNode = values[i]; + const value = valueNode.value; + const unprefixedMediaFeatureName = postcss.vendor.unprefixed(mediaFeatureName); + + const featureWhitelist = _.find(whitelist, (v, whitelistFeatureName) => + matchesStringOrRegExp(unprefixedMediaFeatureName, whitelistFeatureName), + ); + + if (featureWhitelist === undefined) { + return; + } + + if (matchesStringOrRegExp(value, featureWhitelist)) { + return; + } + + report({ + index: atRuleParamIndex(atRule) + valueNode.sourceIndex, + message: messages.rejected(mediaFeatureName, value), + node: atRule, + ruleName, + result, + }); + } }); }); }; diff --git a/lib/rules/media-feature-name-whitelist/README.md b/lib/rules/media-feature-name-whitelist/README.md index 12f7ece739..44ca89f100 100644 --- a/lib/rules/media-feature-name-whitelist/README.md +++ b/lib/rules/media-feature-name-whitelist/README.md @@ -8,8 +8,6 @@ Specify a whitelist of allowed media feature names. * This media feature name */ ``` -This rule ignores media feature names within a range context. - ## Options `array|string|regex`: `["array", "of", "unprefixed", /media-features/ or "regex"]|"media-feature"|/regex/` @@ -30,6 +28,14 @@ The following patterns are considered violations: @media print and (min-resolution: 300dpi) {} ``` +```css +@media (min-width < 50em) {} +``` + +```css +@media (10em < min-width < 50em) {} +``` + The following patterns are *not* considered violations: ```css @@ -39,3 +45,11 @@ The following patterns are *not* considered violations: ```css @media (my-width: 50em) {} ``` + +```css +@media (max-width > 50em) {} +``` + +```css +@media (10em < my-width < 50em) {} +``` diff --git a/lib/rules/media-feature-name-whitelist/__tests__/index.js b/lib/rules/media-feature-name-whitelist/__tests__/index.js index 4428f9c88c..cdea1e41ee 100644 --- a/lib/rules/media-feature-name-whitelist/__tests__/index.js +++ b/lib/rules/media-feature-name-whitelist/__tests__/index.js @@ -20,12 +20,10 @@ testRule(rule, { description: 'ignore comments', }, { - code: '@media (width <= 50em) { }', - description: 'ignore media features in a range context', + code: '@media (max-width <= 50em) { }', }, { - code: '@media (400px < width < 1000px) { }', - description: 'ignore media features in a range context', + code: '@media (400px < my-width < 1000px) { }', }, { code: '@media (my-width: 50em) { }', @@ -72,6 +70,30 @@ testRule(rule, { line: 1, column: 9, }, + { + code: '@media (width: 50em) { }', + message: messages.rejected('width'), + line: 1, + column: 9, + }, + { + code: '@media (50em < width) { }', + message: messages.rejected('width'), + line: 1, + column: 16, + }, + { + code: '@media (10em < width <= 50em) { }', + message: messages.rejected('width'), + line: 1, + column: 16, + }, + { + code: '@media (max-width <= 50em) and (10em < min-width < 50em) { }', + message: messages.rejected('min-width'), + line: 1, + column: 40, + }, ], }); @@ -86,6 +108,12 @@ testRule(rule, { { code: '@media (my-max-width: 50em) { }', }, + { + code: '@media (my-width >= 50em) { }', + }, + { + code: '@media (10em < my-max-width <= 50em) { }', + }, ], reject: [ diff --git a/lib/rules/media-feature-name-whitelist/index.js b/lib/rules/media-feature-name-whitelist/index.js index e91d157559..c3af91f509 100644 --- a/lib/rules/media-feature-name-whitelist/index.js +++ b/lib/rules/media-feature-name-whitelist/index.js @@ -7,6 +7,7 @@ const isRangeContextMediaFeature = require('../../utils/isRangeContextMediaFeatu const isStandardSyntaxMediaFeatureName = require('../../utils/isStandardSyntaxMediaFeatureName'); const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp'); const mediaParser = require('postcss-media-query-parser').default; +const rangeContextNodeParser = require('../rangeContextNodeParser'); const report = require('../../utils/report'); const ruleMessages = require('../../utils/ruleMessages'); const validateOptions = require('../../utils/validateOptions'); @@ -31,14 +32,22 @@ function rule(whitelist) { root.walkAtRules(/^media$/i, (atRule) => { mediaParser(atRule.params).walk(/^media-feature$/i, (mediaFeatureNode) => { const parent = mediaFeatureNode.parent; - const sourceIndex = mediaFeatureNode.sourceIndex; - const value = mediaFeatureNode.value; + const mediaFeatureRangeContext = isRangeContextMediaFeature(parent.value); - if ( - isRangeContextMediaFeature(parent.value) || - !isStandardSyntaxMediaFeatureName(value) || - isCustomMediaQuery(value) - ) { + let value; + let sourceIndex; + + if (mediaFeatureRangeContext) { + const parsedRangeContext = rangeContextNodeParser(mediaFeatureNode); + + value = parsedRangeContext.name.value; + sourceIndex = parsedRangeContext.name.sourceIndex; + } else { + value = mediaFeatureNode.value; + sourceIndex = mediaFeatureNode.sourceIndex; + } + + if (!isStandardSyntaxMediaFeatureName(value) || isCustomMediaQuery(value)) { return; } diff --git a/lib/rules/rangeContextNodeParser.js b/lib/rules/rangeContextNodeParser.js new file mode 100644 index 0000000000..19f666a8e2 --- /dev/null +++ b/lib/rules/rangeContextNodeParser.js @@ -0,0 +1,63 @@ +'use strict'; + +const rangeOperators = ['>=', '<=', '>', '<', '=']; +const styleSearch = require('style-search'); + +function getRangeContextOperators(node) { + const operators = []; + + styleSearch({ source: node.value, target: rangeOperators }, (match) => { + const before = node[match.startIndex - 1]; + + if (before === '>' || before === '<') { + return; + } + + operators.push(match.target); + }); + + // Sorting helps when using the operators to split + // E.g. for "(10em < width <= 50em)" this returns ["<=", "<"] + return operators.sort((a, b) => b.length - a.length); +} + +function getRangeContextName(parsedNode) { + // When the node is like "(10em < width < 50em)" + // The parsedNode is ["10em", "width", "50em"] - the name is always in the second position + if (parsedNode.length === 3) { + return parsedNode[1]; + } + + // When the node is like "(width > 10em)" or "(10em < width)" + // Regex is needed because the name can either be in the first or second position + return parsedNode.find((value) => value.match(/^(?!--)\D+/) || value.match(/^(--).+/)); +} + +module.exports = function(node) { + const nodeValue = node.value; + + const operators = getRangeContextOperators(node); + + // Remove spaces and parentheses and split by the operators + const parsedMedia = nodeValue.replace(/[()\s]/g, '').split(new RegExp(operators.join('|'))); + + const name = getRangeContextName(parsedMedia); + const nameObj = { + value: name, + sourceIndex: node.sourceIndex + nodeValue.indexOf(name), + }; + + const values = parsedMedia + .filter((parsedValue) => parsedValue !== name) + .map((value) => { + return { + value, + sourceIndex: node.sourceIndex + nodeValue.indexOf(value), + }; + }); + + return { + name: nameObj, + values, + }; +}; diff --git a/system-tests/005/__snapshots__/005.test.js.snap b/system-tests/005/__snapshots__/005.test.js.snap index fba562e3f5..2f5ce526ac 100644 --- a/system-tests/005/__snapshots__/005.test.js.snap +++ b/system-tests/005/__snapshots__/005.test.js.snap @@ -11347,6 +11347,20 @@ Array [ "severity": "error", "text": "Unexpected media feature name \\"max-width\\" (media-feature-name-blacklist)", }, + Object { + "column": 9, + "line": 139, + "rule": "media-feature-name-blacklist", + "severity": "error", + "text": "Unexpected media feature name \\"max-width\\" (media-feature-name-blacklist)", + }, + Object { + "column": 9, + "line": 140, + "rule": "media-feature-name-blacklist", + "severity": "error", + "text": "Unexpected media feature name \\"max-width\\" (media-feature-name-blacklist)", + }, Object { "column": 9, "line": 133,