securityos/node_modules/stylelint/lib/rules/declaration-block-no-redund.../index.js

336 lines
9.7 KiB
JavaScript

'use strict';
const valueParser = require('postcss-value-parser');
const arrayEqual = require('../../utils/arrayEqual');
const { basicKeywords } = require('../../reference/keywords');
const eachDeclarationBlock = require('../../utils/eachDeclarationBlock');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const { longhandSubPropertiesOfShorthandProperties } = require('../../reference/properties');
const validateOptions = require('../../utils/validateOptions');
const vendor = require('../../utils/vendor');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'declaration-block-no-redundant-longhand-properties';
const messages = ruleMessages(ruleName, {
expected: (props) => `Expected shorthand property "${props}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-block-no-redundant-longhand-properties',
fixable: true,
};
/** @typedef {import('postcss').Declaration} Declaration */
/** @type {Map<string, (decls: Map<string, Declaration>) => (string | undefined)>} */
const customResolvers = new Map([
[
'font-synthesis',
(decls) => {
const weight = decls.get('font-synthesis-weight')?.value.trim();
const style = decls.get('font-synthesis-style')?.value.trim();
const smallCaps = decls.get('font-synthesis-small-caps')?.value.trim();
/** @type {(s: string | undefined) => boolean} */
const isValidFontSynthesisValue = (s) => s === 'none' || s === 'auto';
if (
!isValidFontSynthesisValue(weight) ||
!isValidFontSynthesisValue(style) ||
!isValidFontSynthesisValue(smallCaps)
) {
return;
}
const autoShorthands = [];
if (weight === 'auto') {
autoShorthands.push('weight');
}
if (style === 'auto') {
autoShorthands.push('style');
}
if (smallCaps === 'auto') {
autoShorthands.push('small-caps');
}
if (autoShorthands.length === 0) return 'none';
return autoShorthands.join(' ');
},
],
[
'grid-column',
(decls) => {
const start = decls.get('grid-column-start')?.value.trim();
const end = decls.get('grid-column-end')?.value.trim();
if (!start || !end) return;
return `${start} / ${end}`;
},
],
[
'grid-row',
(decls) => {
const start = decls.get('grid-row-start')?.value.trim();
const end = decls.get('grid-row-end')?.value.trim();
if (!start || !end) return;
return `${start} / ${end}`;
},
],
[
'grid-template',
(decls) => {
const areas = decls.get('grid-template-areas')?.value.trim();
const columns = decls.get('grid-template-columns')?.value.trim();
const rows = decls.get('grid-template-rows')?.value.trim();
if (!(areas && columns && rows)) return;
// repeat() is not allowed inside track listings for grid-template.
// related issue: https://github.com/stylelint/stylelint/issues/7228
// spec ref: https://drafts.csswg.org/css-grid/#explicit-grid-shorthand
if (columns.includes('repeat(') || rows.includes('repeat(')) return;
const splitAreas = [...areas.matchAll(/"[^"]+"/g)].map((x) => x[0]);
const splitRows = rows.split(' ');
if (splitAreas.length === 0 || splitRows.length === 0) return;
if (splitAreas.length !== splitRows.length) return;
const zipped = splitAreas.map((area, i) => `${area} ${splitRows[i]}`).join(' ');
return `${zipped} / ${columns}`;
},
],
[
'transition',
(decls) => {
/** @type {(input: string | undefined) => string[]} */
const commaSeparated = (input = '') => {
let trimmedInput = input.trim();
if (!trimmedInput) return [];
if (trimmedInput.indexOf(',') === -1) return [trimmedInput];
/** @type {import('postcss-value-parser').ParsedValue} */
let parsedValue = valueParser(trimmedInput);
/** @type {Array<Array<import('postcss-value-parser').Node>>} */
let valueParts = [];
{
/** @type {Array<import('postcss-value-parser').Node>} */
let currentListItem = [];
parsedValue.nodes.forEach((node) => {
if (node.type === 'div' && node.value === ',') {
valueParts.push(currentListItem);
currentListItem = [];
return;
}
currentListItem.push(node);
});
valueParts.push(currentListItem);
}
return valueParts.map((s) => valueParser.stringify(s).trim()).filter((s) => s.length > 0);
};
const delays = commaSeparated(decls.get('transition-delay')?.value);
const durations = commaSeparated(decls.get('transition-duration')?.value);
const timingFunctions = commaSeparated(decls.get('transition-timing-function')?.value);
const properties = commaSeparated(decls.get('transition-property')?.value);
if (!(delays.length && durations.length && timingFunctions.length && properties.length)) {
return;
}
// transition-property is the canonical list of the number of properties;
// see spec: https://w3c.github.io/csswg-drafts/css-transitions/#transition-property-property
// if there are more transition-properties than duration/delay/timings,
// the other properties are computed cyclically -- ex with %
// see spec example #3: https://w3c.github.io/csswg-drafts/css-transitions/#example-d94cbd75
return properties
.map((property, i) => {
return [
property,
durations[i % durations.length],
timingFunctions[i % timingFunctions.length],
delays[i % delays.length],
]
.filter(isString)
.join(' ');
})
.join(', ');
},
],
]);
/**
* @param {string} prefixedShorthandProperty
* @param {string[]} prefixedShorthandData
* @param {Map<string, Declaration>} transformedDeclarationNodes
* @returns {string | undefined}
*/
const resolveShorthandValue = (
prefixedShorthandProperty,
prefixedShorthandData,
transformedDeclarationNodes,
) => {
const resolver = customResolvers.get(prefixedShorthandProperty);
if (resolver === undefined) {
// the "default" resolver: sort the longhand values in the order
// of their properties
const values = prefixedShorthandData
.map((p) => transformedDeclarationNodes.get(p)?.value.trim())
.filter(Boolean);
return values.length > 0 ? values.join(' ') : undefined;
}
return resolver(transformedDeclarationNodes);
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{ actual: primary },
{
actual: secondaryOptions,
possible: {
ignoreShorthands: [isString, isRegExp],
},
optional: true,
},
);
if (!validOptions) {
return;
}
/** @type {Map<string, import('stylelint').ShorthandProperties[]>} */
const longhandToShorthands = new Map();
for (const [shorthand, longhandProps] of longhandSubPropertiesOfShorthandProperties.entries()) {
if (optionsMatches(secondaryOptions, 'ignoreShorthands', shorthand)) {
continue;
}
for (const longhand of longhandProps) {
const shorthands = longhandToShorthands.get(longhand) || [];
shorthands.push(shorthand);
longhandToShorthands.set(longhand, shorthands);
}
}
eachDeclarationBlock(root, (eachDecl) => {
/** @type {Map<string, string[]>} */
const longhandDeclarations = new Map();
/** @type {Map<string, Declaration[]>} */
const longhandDeclarationNodes = new Map();
eachDecl((decl) => {
// basic keywords are not allowed in shorthand properties
if (basicKeywords.has(decl.value)) {
return;
}
const prop = decl.prop.toLowerCase();
const unprefixedProp = vendor.unprefixed(prop);
const prefix = vendor.prefix(prop);
const shorthandProperties = longhandToShorthands.get(unprefixedProp);
if (!shorthandProperties) {
return;
}
for (const shorthandProperty of shorthandProperties) {
const prefixedShorthandProperty = prefix + shorthandProperty;
const longhandDeclaration = longhandDeclarations.get(prefixedShorthandProperty) || [];
const longhandDeclarationNode =
longhandDeclarationNodes.get(prefixedShorthandProperty) || [];
longhandDeclaration.push(prop);
longhandDeclarations.set(prefixedShorthandProperty, longhandDeclaration);
longhandDeclarationNode.push(decl);
longhandDeclarationNodes.set(prefixedShorthandProperty, longhandDeclarationNode);
const shorthandProps = longhandSubPropertiesOfShorthandProperties.get(shorthandProperty);
const prefixedShorthandData = Array.from(shorthandProps || [], (item) => prefix + item);
const copiedPrefixedShorthandData = [...prefixedShorthandData];
if (!arrayEqual(copiedPrefixedShorthandData.sort(), longhandDeclaration.sort())) {
continue;
}
if (context.fix) {
const declNodes = longhandDeclarationNodes.get(prefixedShorthandProperty) || [];
const [firstDeclNode] = declNodes;
if (firstDeclNode) {
const transformedDeclarationNodes = new Map(
declNodes.map((d) => [d.prop.toLowerCase(), d]),
);
const resolvedShorthandValue = resolveShorthandValue(
prefixedShorthandProperty,
prefixedShorthandData,
transformedDeclarationNodes,
);
if (resolvedShorthandValue) {
const newShorthandDeclarationNode = firstDeclNode.clone({
prop: prefixedShorthandProperty,
value: resolvedShorthandValue,
});
firstDeclNode.replaceWith(newShorthandDeclarationNode);
declNodes.forEach((node) => node.remove());
return;
}
}
}
report({
ruleName,
result,
node: decl,
word: decl.prop,
message: messages.expected,
messageArgs: [prefixedShorthandProperty],
});
}
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;