stylelint
Version:
A mighty CSS linter that helps you avoid errors and enforce conventions.
272 lines (217 loc) • 8.69 kB
JavaScript
// NOTICE: This file is generated by Rollup. To modify it,
// please instead edit the ESM counterpart and rebuild with Rollup (npm run build).
'use strict';
const cssTokenizer = require('@csstools/css-tokenizer');
const cssParserAlgorithms = require('@csstools/css-parser-algorithms');
const mediaQueryListParser = require('@csstools/media-query-list-parser');
const units = require('../../reference/units.cjs');
const mediaFeatures = require('../../reference/mediaFeatures.cjs');
const functions = require('../../reference/functions.cjs');
const nodeFieldIndices = require('../../utils/nodeFieldIndices.cjs');
const regexes = require('../../utils/regexes.cjs');
const parseMediaQuery = require('../../utils/parseMediaQuery.cjs');
const report = require('../../utils/report.cjs');
const ruleMessages = require('../../utils/ruleMessages.cjs');
const validateOptions = require('../../utils/validateOptions.cjs');
const vendor = require('../../utils/vendor.cjs');
const ruleName = 'media-feature-name-value-no-unknown';
const messages = ruleMessages(ruleName, {
rejected: (name, value) => `Unexpected unknown media feature value "${value}" for name "${name}"`,
});
const HAS_MIN_MAX_PREFIX = /^(?:min|max)-/i;
const meta = {
url: 'https://stylelint.io/user-guide/rules/media-feature-name-value-no-unknown',
};
/** @typedef {{ mediaFeatureName: string, mediaFeatureNameRaw: string }} State */
/** @typedef { (state: State, valuePart: string, start: number, end: number) => void } Reporter */
/** @type {import('stylelint').CoreRules[ruleName]} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual: primary });
if (!validOptions) {
return;
}
/**
* Check that a single token value is valid for a given media feature name.
*
* @param {State} state
* @param {import('@csstools/css-tokenizer').CSSToken} token
* @param {Reporter} reporter
* @returns {void}
*/
function checkSingleToken(state, token, reporter) {
const [type, raw, start, end, parsed] = token;
if (type === cssTokenizer.TokenType.Ident) {
const supportedKeywords = mediaFeatures.mediaFeatureNameAllowedValueKeywords.get(state.mediaFeatureName);
if (supportedKeywords) {
const keyword = vendor.unprefixed(parsed.value.toLowerCase());
if (supportedKeywords.has(keyword)) return;
}
// An ident that isn't expected for the given media feature name
reporter(state, raw, start, end);
return;
}
const supportedValueTypes = mediaFeatures.mediaFeatureNameAllowedValueTypes.get(state.mediaFeatureName);
if (!supportedValueTypes) {
// The given media feature name doesn't support any single token values.
reporter(state, raw, start, end);
return;
}
if (type === cssTokenizer.TokenType.Number) {
if (parsed.type === cssTokenizer.NumberType.Integer) {
if (
// Integer values are valid for types "integer" and "ratio".
supportedValueTypes.has('integer') ||
supportedValueTypes.has('ratio') ||
// Integer values of "0" are also valid for "length", "resolution" and "mq-boolean".
(parsed.value === 0 &&
(supportedValueTypes.has('length') ||
supportedValueTypes.has('resolution') ||
supportedValueTypes.has('mq-boolean'))) ||
// Integer values of "1" are also valid for "mq-boolean".
(parsed.value === 1 && supportedValueTypes.has('mq-boolean'))
) {
return;
}
// An integer when the media feature doesn't support integers.
reporter(state, raw, start, end);
return;
}
if (
// Numbers are valid for "ratio".
supportedValueTypes.has('ratio') ||
// Numbers with value "0" are also valid for "length".
(parsed.value === 0 &&
(supportedValueTypes.has('length') || supportedValueTypes.has('resolution')))
) {
return;
}
// A number when the media feature doesn't support numbers.
reporter(state, raw, start, end);
return;
}
if (type === cssTokenizer.TokenType.Dimension) {
const unit = parsed.unit.toLowerCase();
if (supportedValueTypes.has('resolution') && units.resolutionUnits.has(unit)) return;
if (supportedValueTypes.has('length') && units.lengthUnits.has(unit)) return;
// An unexpected dimension or a media feature that doesn't support dimensions.
reporter(state, raw, start, end);
}
}
/**
* Check that a function node is valid for a given media feature name.
*
* @param {State} state
* @param {import('@csstools/css-parser-algorithms').FunctionNode} functionNode
* @param {Reporter} reporter
* @returns {void}
*/
function checkFunction(state, functionNode, reporter) {
const functionName = functionNode.getName().toLowerCase();
// "env()" can represent any value, it is treated as valid for static analysis.
if (functionName === 'env') return;
const supportedValueTypes = mediaFeatures.mediaFeatureNameAllowedValueTypes.get(state.mediaFeatureName);
if (
supportedValueTypes &&
functions.mathFunctions.has(functionName) &&
(supportedValueTypes.has('integer') ||
supportedValueTypes.has('length') ||
supportedValueTypes.has('ratio') ||
supportedValueTypes.has('resolution'))
) {
return;
}
// An unexpected function or a media feature that doesn't support types that can be the result of a function.
reporter(state, functionNode.toString(), ...cssParserAlgorithms.sourceIndices(functionNode));
}
/**
* Check that an array of component values is valid for a given media feature name.
*
* @param {State} state
* @param {Array<import('@csstools/css-parser-algorithms').ComponentValue>} componentValues
* @param {Reporter} reporter
* @returns {void}
*/
function checkListOfComponentValues(state, componentValues, reporter) {
const supportedValueTypes = mediaFeatures.mediaFeatureNameAllowedValueTypes.get(state.mediaFeatureName);
if (
supportedValueTypes &&
supportedValueTypes.has('ratio') &&
mediaQueryListParser.matchesRatioExactly(componentValues) !== -1
) {
return;
}
// An invalid aspect ratio or a media feature that doesn't support aspect ratios.
reporter(
state,
componentValues.map((x) => x.toString()).join(''),
...cssParserAlgorithms.sourceIndices(componentValues),
);
}
/**
* @param {State} state
* @param {import('@csstools/media-query-list-parser').MediaFeatureValue} valueNode
* @param {Reporter} reporter
* @returns {void}
*/
function checkMediaFeatureValue(state, valueNode, reporter) {
if (cssParserAlgorithms.isTokenNode(valueNode.value)) {
checkSingleToken(state, valueNode.value.value, reporter);
return;
}
if (cssParserAlgorithms.isFunctionNode(valueNode.value)) {
checkFunction(state, valueNode.value, reporter);
return;
}
if (Array.isArray(valueNode.value)) {
checkListOfComponentValues(state, valueNode.value, reporter);
}
}
root.walkAtRules(regexes.atRuleRegexes.mediaName, (atRule) => {
/**
* @type {Reporter}
*/
const reporter = (state, valuePart, start, end) => {
const atRuleParamIndexValue = nodeFieldIndices.atRuleParamIndex(atRule);
report({
message: messages.rejected,
messageArgs: [state.mediaFeatureNameRaw, valuePart],
index: atRuleParamIndexValue + start,
endIndex: atRuleParamIndexValue + end + 1,
node: atRule,
ruleName,
result,
});
};
/** @type {State} */
const initialState = {
mediaFeatureName: '',
mediaFeatureNameRaw: '',
};
parseMediaQuery(atRule).forEach((mediaQuery) => {
if (mediaQueryListParser.isMediaQueryInvalid(mediaQuery)) return;
mediaQuery.walk(({ node, state }) => {
if (!state) return;
if (mediaQueryListParser.isMediaFeature(node)) {
const mediaFeatureNameRaw = node.getName();
let mediaFeatureName = vendor.unprefixed(mediaFeatureNameRaw.toLowerCase());
// Unknown media feature names are handled by "media-feature-name-no-unknown".
if (!mediaFeatures.mediaFeatureNames.has(mediaFeatureName)) return;
mediaFeatureName = mediaFeatureName.replace(HAS_MIN_MAX_PREFIX, '');
state.mediaFeatureName = mediaFeatureName;
state.mediaFeatureNameRaw = mediaFeatureNameRaw;
return;
}
if (!state.mediaFeatureName || !state.mediaFeatureNameRaw) return;
if (mediaQueryListParser.isMediaFeatureValue(node)) {
checkMediaFeatureValue(state, node, reporter);
}
}, initialState);
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;