Skip to content

Commit

Permalink
Refactor media-feature-name-value-allowed-list to replace `postcss-…
Browse files Browse the repository at this point in the history
…media-query-parser` with `@csstools/media-query-list-parser` (#6999)


Co-authored-by: Masafumi Koba <473530+ybiquitous@users.noreply.github.com>
  • Loading branch information
romainmenke and ybiquitous committed Jun 30, 2023
1 parent 643576c commit 8051822
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 165 deletions.
5 changes: 5 additions & 0 deletions .changeset/fifty-hotels-admire.md
@@ -0,0 +1,5 @@
---
"stylelint": patch
---

Removed: `postcss-media-query-parser` dependency
2 changes: 1 addition & 1 deletion docs/user-guide/errors.md
Expand Up @@ -65,7 +65,7 @@ The CSS parser built into Stylelint (or the chosen [custom syntax](options.md#cu

The construct-specific parsers are:

- `postcss-media-query-parser`
- `@csstools/media-query-list-parser`
- `postcss-selector-parser`
- `postcss-value-parser`

Expand Down
31 changes: 15 additions & 16 deletions lib/rules/media-feature-name-value-allowed-list/__tests__/index.js
Expand Up @@ -6,7 +6,7 @@ testRule({
ruleName,
config: [
{
'min-width': ['768px', '$sm'],
'min-width': ['768px'],
'/resolution/': '/dpcm$/', // Only dpcm unit
color: [], // Test boolean context
width: [], // Test range context
Expand Down Expand Up @@ -35,8 +35,8 @@ testRule({
description: 'Floating point value',
},
{
code: '@media screen and (min-width: $sm) {}',
description: 'Non-standard syntax in allowed list',
code: '@media screen and (-webkit-resolution: 10.1dpcm) {}',
description: 'Prefixed media feature',
},
{
code: '@media (color) {}',
Expand Down Expand Up @@ -66,16 +66,16 @@ testRule({
endColumn: 37,
},
{
code: '@media screen (min-width: 768px) and (min-width: 1000px) {}',
code: '@media screen and (min-width: 768px) and (min-width: 1000px) {}',
description: 'Media feature multiple',
message: messages.rejected('min-width', '1000px'),
line: 1,
column: 50,
column: 54,
endLine: 1,
endColumn: 56,
endColumn: 60,
},
{
code: '@media screen (min-width: 768px)\nand (min-width: 1000px) {}',
code: '@media screen and (min-width: 768px)\nand (min-width: 1000px) {}',
description: 'Media feature multiline',
message: messages.rejected('min-width', '1000px'),
line: 2,
Expand All @@ -92,15 +92,6 @@ testRule({
endLine: 1,
endColumn: 36,
},
{
code: '@media screen and (min-width: $md) {}',
description: 'Non-standard syntax NOT in allowed list',
message: messages.rejected('min-width', '$md'),
line: 1,
column: 31,
endLine: 1,
endColumn: 34,
},
{
code: '@media screen and (min-resolution: 2dpi) {}',
message: messages.rejected('min-resolution', '2dpi'),
Expand All @@ -109,6 +100,14 @@ testRule({
endLine: 1,
endColumn: 41,
},
{
code: '@media screen and (-webkit-min-resolution: 2dpi) {}',
message: messages.rejected('-webkit-min-resolution', '2dpi'),
line: 1,
column: 45,
endLine: 1,
endColumn: 49,
},
{
code: '@media screen and (min-width > 500px) {}',
message: messages.rejected('min-width', '500px'),
Expand Down
99 changes: 64 additions & 35 deletions lib/rules/media-feature-name-value-allowed-list/index.js
@@ -1,18 +1,21 @@
'use strict';

const mediaParser = require('postcss-media-query-parser').default;
const {
isMediaQueryInvalid,
isMediaFeature,
isMediaFeatureValue,
} = require('@csstools/media-query-list-parser');

const atRuleParamIndex = require('../../utils/atRuleParamIndex');
const isRangeContextMediaFeature = require('../../utils/isRangeContextMediaFeature');
const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp');
const optionsMatches = require('../../utils/optionsMatches');
const rangeContextNodeParser = require('../rangeContextNodeParser');
const parseMediaQuery = require('../../utils/parseMediaQuery');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateObjectWithArrayProps = require('../../utils/validateObjectWithArrayProps');
const validateOptions = require('../../utils/validateOptions');
const { isString, isRegExp } = require('../../utils/validateTypes');
const vendor = require('../../utils/vendor');
const { isString, isRegExp } = require('../../utils/validateTypes');

const ruleName = 'media-feature-name-value-allowed-list';

Expand All @@ -37,40 +40,32 @@ const rule = (primary) => {
}

root.walkAtRules(/^media$/i, (atRule) => {
mediaParser(atRule.params).walk(/^media-feature-expression$/i, (node) => {
if (!node.nodes) return;

const mediaFeatureRangeContext = isRangeContextMediaFeature(node.parent.value);

// Ignore boolean
if (!node.value.includes(':') && !mediaFeatureRangeContext) {
return;
}
parseMediaQuery(atRule).forEach((mediaQuery) => {
if (isMediaQueryInvalid(mediaQuery)) return;

const mediaFeatureNode = node.nodes.find((n) => n.type === 'media-feature');
const initialState = {
mediaFeatureName: '',
unprefixedMediaFeatureName: '',
};

if (mediaFeatureNode == null) throw new Error('A `media-feature` node must be present');
mediaQuery.walk(({ node, state }) => {
if (!state) return;

let mediaFeatureName;
let values;
if (isMediaFeature(node)) {
state.mediaFeatureName = node.getName();
state.unprefixedMediaFeatureName = vendor.unprefixed(node.getName());

if (mediaFeatureRangeContext) {
const parsedRangeContext = rangeContextNodeParser(mediaFeatureNode);
return;
}

mediaFeatureName = parsedRangeContext.name.value;
values = parsedRangeContext.values;
} else {
mediaFeatureName = mediaFeatureNode.value;
const valueNode = node.nodes.find((n) => n.type === 'value');
if (!isMediaFeatureValue(node)) return;

if (valueNode == null) throw new Error('A `value` node must be present');
const { mediaFeatureName, unprefixedMediaFeatureName } = state;

values = [valueNode];
}
if (!mediaFeatureName || !unprefixedMediaFeatureName) return;

for (const valueNode of values) {
const value = valueNode.value;
const unprefixedMediaFeatureName = vendor.unprefixed(mediaFeatureName);
const componentValues = [node.value].flat();
const value = componentValues.map((x) => x.toString()).join('');

const allowedValuesKey = Object.keys(primary).find((featureName) =>
matchesStringOrRegExp(unprefixedMediaFeatureName, featureName),
Expand All @@ -84,24 +79,58 @@ const rule = (primary) => {
return;
}

const index = atRuleParamIndex(atRule) + valueNode.sourceIndex;
const endIndex = index + value.length;
const atRuleIndex = atRuleParamIndex(atRule);
const [startIndex, endIndex] = startAndEndIndex(componentValues);

report({
index,
endIndex,
index: atRuleIndex + startIndex,
endIndex: atRuleIndex + endIndex + 1,
message: messages.rejected,
messageArgs: [mediaFeatureName, value],
node: atRule,
ruleName,
result,
});
}
}, initialState);
});
});
};
};

/**
* A recursive function that returns the start and end boundaries of a node.
*
* @template {import('@csstools/css-tokenizer').CSSToken} T
* @param {Array<{ tokens(): Array<T> }> | { tokens(): Array<T> }} node
* @returns {[number, number]}
*/
function startAndEndIndex(node) {
if (Array.isArray(node)) {
const nodeStart = node[0];

if (!nodeStart) return [0, 0];

const nodeEnd = node[node.length - 1] || nodeStart;

const [startA] = startAndEndIndex(nodeStart);
const [, endB] = startAndEndIndex(nodeEnd);

return [startA, endB];
}

const tokens = node.tokens();

const firstToken = tokens[0];
const lastToken = tokens[tokens.length - 1];

if (!firstToken || !lastToken) return [0, 0];

const start = firstToken[2];
const end = lastToken[3];

return [start, end];
}

rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
Expand Down
59 changes: 0 additions & 59 deletions lib/rules/rangeContextNodeParser.js

This file was deleted.

17 changes: 0 additions & 17 deletions lib/utils/__tests__/isRangeContextMediaFeature.test.js

This file was deleted.

11 changes: 0 additions & 11 deletions lib/utils/isRangeContextMediaFeature.js

This file was deleted.

24 changes: 0 additions & 24 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 8051822

Please sign in to comment.