fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

View File

@@ -0,0 +1,214 @@
'use strict';
const valueParser = require('postcss-value-parser');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const setDeclarationValue = require('../../utils/setDeclarationValue');
const validateOptions = require('../../utils/validateOptions');
const { isRegExp, isString, assert } = require('../../utils/validateTypes');
const ruleName = 'alpha-value-notation';
const messages = ruleMessages(ruleName, {
expected: (unfixed, fixed) => `Expected "${unfixed}" to be "${fixed}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/alpha-value-notation',
fixable: true,
};
const ALPHA_PROPS = new Set([
'opacity',
'shape-image-threshold',
// SVG properties
'fill-opacity',
'flood-opacity',
'stop-opacity',
'stroke-opacity',
]);
const ALPHA_FUNCS = new Set(['hsl', 'hsla', 'hwb', 'lab', 'lch', 'rgb', 'rgba']);
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: ['number', 'percentage'],
},
{
actual: secondaryOptions,
possible: {
exceptProperties: [isString, isRegExp],
},
optional: true,
},
);
if (!validOptions) return;
const optionFuncs = Object.freeze({
number: {
expFunc: isNumber,
fixFunc: asNumber,
},
percentage: {
expFunc: isPercentage,
fixFunc: asPercentage,
},
});
root.walkDecls((decl) => {
let needsFix = false;
const parsedValue = valueParser(getDeclarationValue(decl));
parsedValue.walk((node) => {
/** @type {import('postcss-value-parser').Node | undefined} */
let alpha;
if (ALPHA_PROPS.has(decl.prop.toLowerCase())) {
alpha = findAlphaInValue(node);
} else {
if (node.type !== 'function') return;
if (!ALPHA_FUNCS.has(node.value.toLowerCase())) return;
alpha = findAlphaInFunction(node);
}
if (!alpha) return;
const { value } = alpha;
if (!isStandardSyntaxValue(value)) return;
if (!isNumber(value) && !isPercentage(value)) return;
/** @type {'number' | 'percentage'} */
let expectation = primary;
if (optionsMatches(secondaryOptions, 'exceptProperties', decl.prop)) {
if (expectation === 'number') {
expectation = 'percentage';
} else if (expectation === 'percentage') {
expectation = 'number';
}
}
if (optionFuncs[expectation].expFunc(value)) return;
const fixed = optionFuncs[expectation].fixFunc(value);
const unfixed = value;
if (context.fix) {
alpha.value = String(fixed);
needsFix = true;
return;
}
const index = declarationValueIndex(decl) + alpha.sourceIndex;
const endIndex = index + alpha.value.length;
report({
message: messages.expected(unfixed, fixed),
node: decl,
index,
endIndex,
result,
ruleName,
});
});
if (needsFix) {
setDeclarationValue(decl, parsedValue.toString());
}
});
};
};
/**
* @param {string} value
* @returns {string}
*/
function asPercentage(value) {
const number = Number(value);
return `${Number((number * 100).toPrecision(3))}%`;
}
/**
* @param {string} value
* @returns {string}
*/
function asNumber(value) {
const dimension = valueParser.unit(value);
assert(dimension);
const number = Number(dimension.number);
return Number((number / 100).toPrecision(3)).toString();
}
/**
* @template {import('postcss-value-parser').Node} T
* @param {T} node
* @returns {T | undefined}
*/
function findAlphaInValue(node) {
return node.type === 'word' || node.type === 'function' ? node : undefined;
}
/**
* @param {import('postcss-value-parser').FunctionNode} node
* @returns {import('postcss-value-parser').Node | undefined}
*/
function findAlphaInFunction(node) {
const args = node.nodes.filter(({ type }) => type === 'word' || type === 'function');
if (args.length === 4) return args[3];
const slashNodeIndex = node.nodes.findIndex(({ type, value }) => type === 'div' && value === '/');
if (slashNodeIndex !== -1) {
const nodesAfterSlash = node.nodes.slice(slashNodeIndex + 1, node.nodes.length);
return nodesAfterSlash.find(({ type }) => type === 'word');
}
return undefined;
}
/**
* @param {string} value
* @returns {boolean}
*/
function isPercentage(value) {
const dimension = valueParser.unit(value);
return dimension && dimension.unit === '%';
}
/**
* @param {string} value
* @returns {boolean}
*/
function isNumber(value) {
const dimension = valueParser.unit(value);
return dimension && dimension.unit === '';
}
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,86 @@
'use strict';
const valueParser = require('postcss-value-parser');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'annotation-no-unknown';
const messages = ruleMessages(ruleName, {
rejected: (annotation) => `Unexpected unknown annotation "${annotation}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/annotation-no-unknown',
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{ actual: primary },
{
actual: secondaryOptions,
possible: {
ignoreAnnotations: [isString, isRegExp],
},
optional: true,
},
);
if (!validOptions) {
return;
}
root.walkDecls(checkStatement);
/**
* @param {import('postcss').Declaration} decl
*/
function checkStatement(decl) {
if (decl.important) return;
if (!decl.value.includes('!')) return;
const parsedValue = valueParser(getDeclarationValue(decl));
parsedValue.walk((node) => {
if (!isAnnotation(node)) return;
const value = node.value;
const tokenValue = value.slice(1);
if (optionsMatches(secondaryOptions, 'ignoreAnnotations', tokenValue)) {
return;
}
report({
message: messages.rejected(value),
node: decl,
result,
ruleName,
word: value,
});
});
}
/**
* @param {valueParser.Node} node
*/
function isAnnotation(node) {
return node.type === 'word' && node.value.startsWith('!');
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,61 @@
'use strict';
const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const vendor = require('../../utils/vendor');
const { isString } = require('../../utils/validateTypes');
const ruleName = 'at-rule-allowed-list';
const messages = ruleMessages(ruleName, {
rejected: (name) => `Unexpected at-rule "${name}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/at-rule-allowed-list',
};
/** @type {import('stylelint').Rule<string | string[]>} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [isString],
});
if (!validOptions) {
return;
}
const primaryValues = [primary].flat();
root.walkAtRules((atRule) => {
const name = atRule.name;
if (!isStandardSyntaxAtRule(atRule)) {
return;
}
if (primaryValues.includes(vendor.unprefixed(name).toLowerCase())) {
return;
}
report({
message: messages.rejected(name),
node: atRule,
result,
ruleName,
word: `@${name}`,
});
});
};
};
rule.primaryOptionArray = true;
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,62 @@
'use strict';
const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const vendor = require('../../utils/vendor');
const { isString } = require('../../utils/validateTypes');
const ruleName = 'at-rule-disallowed-list';
const messages = ruleMessages(ruleName, {
rejected: (name) => `Unexpected at-rule "${name}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/at-rule-disallowed-list',
};
/** @type {import('stylelint').Rule<string | string[]>} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [isString],
});
if (!validOptions) {
return;
}
const primaryValues = [primary].flat();
root.walkAtRules((atRule) => {
const name = atRule.name;
if (!isStandardSyntaxAtRule(atRule)) {
return;
}
if (!primaryValues.includes(vendor.unprefixed(name).toLowerCase())) {
return;
}
report({
message: messages.rejected,
messageArgs: [name],
node: atRule,
result,
ruleName,
word: `@${atRule.name}`,
});
});
};
};
rule.primaryOptionArray = true;
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,173 @@
'use strict';
const addEmptyLineBefore = require('../../utils/addEmptyLineBefore');
const getPreviousNonSharedLineCommentNode = require('../../utils/getPreviousNonSharedLineCommentNode');
const hasEmptyLine = require('../../utils/hasEmptyLine');
const isAfterComment = require('../../utils/isAfterComment');
const isBlocklessAtRuleAfterBlocklessAtRule = require('../../utils/isBlocklessAtRuleAfterBlocklessAtRule');
const isBlocklessAtRuleAfterSameNameBlocklessAtRule = require('../../utils/isBlocklessAtRuleAfterSameNameBlocklessAtRule');
const isFirstNested = require('../../utils/isFirstNested');
const isFirstNodeOfRoot = require('../../utils/isFirstNodeOfRoot');
const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
const optionsMatches = require('../../utils/optionsMatches');
const removeEmptyLinesBefore = require('../../utils/removeEmptyLinesBefore');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const { isAtRule } = require('../../utils/typeGuards');
const validateOptions = require('../../utils/validateOptions');
const { isString } = require('../../utils/validateTypes');
const ruleName = 'at-rule-empty-line-before';
const messages = ruleMessages(ruleName, {
expected: 'Expected empty line before at-rule',
rejected: 'Unexpected empty line before at-rule',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/at-rule-empty-line-before',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: ['always', 'never'],
},
{
actual: secondaryOptions,
possible: {
except: [
'after-same-name',
'inside-block',
'blockless-after-same-name-blockless',
'blockless-after-blockless',
'first-nested',
],
ignore: [
'after-comment',
'first-nested',
'inside-block',
'blockless-after-same-name-blockless',
'blockless-after-blockless',
],
ignoreAtRules: [isString],
},
optional: true,
},
);
if (!validOptions) {
return;
}
/** @type {'always' | 'never'} */
const expectation = primary;
root.walkAtRules((atRule) => {
const isNested = atRule.parent && atRule.parent.type !== 'root';
// Ignore the first node
if (isFirstNodeOfRoot(atRule)) {
return;
}
if (!isStandardSyntaxAtRule(atRule)) {
return;
}
// Return early if at-rule is to be ignored
if (optionsMatches(secondaryOptions, 'ignoreAtRules', atRule.name)) {
return;
}
// Optionally ignore the expectation if the node is blockless
if (
optionsMatches(secondaryOptions, 'ignore', 'blockless-after-blockless') &&
isBlocklessAtRuleAfterBlocklessAtRule(atRule)
) {
return;
}
// Optionally ignore the node if it is the first nested
if (optionsMatches(secondaryOptions, 'ignore', 'first-nested') && isFirstNested(atRule)) {
return;
}
// Optionally ignore the expectation if the node is blockless
// and following another blockless at-rule with the same name
if (
optionsMatches(secondaryOptions, 'ignore', 'blockless-after-same-name-blockless') &&
isBlocklessAtRuleAfterSameNameBlocklessAtRule(atRule)
) {
return;
}
// Optionally ignore the expectation if the node is inside a block
if (optionsMatches(secondaryOptions, 'ignore', 'inside-block') && isNested) {
return;
}
// Optionally ignore the expectation if a comment precedes this node
if (optionsMatches(secondaryOptions, 'ignore', 'after-comment') && isAfterComment(atRule)) {
return;
}
const hasEmptyLineBefore = hasEmptyLine(atRule.raws.before);
let expectEmptyLineBefore = expectation === 'always';
// Optionally reverse the expectation if any exceptions apply
if (
(optionsMatches(secondaryOptions, 'except', 'after-same-name') &&
isAtRuleAfterSameNameAtRule(atRule)) ||
(optionsMatches(secondaryOptions, 'except', 'inside-block') && isNested) ||
(optionsMatches(secondaryOptions, 'except', 'first-nested') && isFirstNested(atRule)) ||
(optionsMatches(secondaryOptions, 'except', 'blockless-after-blockless') &&
isBlocklessAtRuleAfterBlocklessAtRule(atRule)) ||
(optionsMatches(secondaryOptions, 'except', 'blockless-after-same-name-blockless') &&
isBlocklessAtRuleAfterSameNameBlocklessAtRule(atRule))
) {
expectEmptyLineBefore = !expectEmptyLineBefore;
}
// Return if the expectation is met
if (expectEmptyLineBefore === hasEmptyLineBefore) {
return;
}
// Fix
if (context.fix && context.newline) {
if (expectEmptyLineBefore) {
addEmptyLineBefore(atRule, context.newline);
} else {
removeEmptyLinesBefore(atRule, context.newline);
}
return;
}
const message = expectEmptyLineBefore ? messages.expected : messages.rejected;
report({ message, node: atRule, result, ruleName });
});
};
};
/**
* @param {import('postcss').AtRule} atRule
*/
function isAtRuleAfterSameNameAtRule(atRule) {
const previousNode = getPreviousNonSharedLineCommentNode(atRule);
return previousNode && isAtRule(previousNode) && previousNode.name === atRule.name;
}
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,66 @@
'use strict';
const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'at-rule-name-case';
const messages = ruleMessages(ruleName, {
expected: (actual, expected) => `Expected "${actual}" to be "${expected}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/at-rule-name-case',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondary, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['lower', 'upper'],
});
if (!validOptions) {
return;
}
/** @type {'lower' | 'upper'} */
const expectation = primary;
root.walkAtRules((atRule) => {
if (!isStandardSyntaxAtRule(atRule)) {
return;
}
const name = atRule.name;
const expectedName = expectation === 'lower' ? name.toLowerCase() : name.toUpperCase();
if (name === expectedName) {
return;
}
if (context.fix) {
atRule.name = expectedName;
return;
}
report({
message: messages.expected(name, expectedName),
node: atRule,
ruleName,
result,
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,44 @@
'use strict';
const atRuleNameSpaceChecker = require('../atRuleNameSpaceChecker');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'at-rule-name-newline-after';
const messages = ruleMessages(ruleName, {
expectedAfter: (name) => `Expected newline after at-rule name "${name}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/at-rule-name-newline-after',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
const checker = whitespaceChecker('newline', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'always-multi-line'],
});
if (!validOptions) {
return;
}
atRuleNameSpaceChecker({
root,
result,
locationChecker: checker.afterOneOnly,
checkedRuleName: ruleName,
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,52 @@
'use strict';
const atRuleNameSpaceChecker = require('../atRuleNameSpaceChecker');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'at-rule-name-space-after';
const messages = ruleMessages(ruleName, {
expectedAfter: (name) => `Expected single space after at-rule name "${name}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/at-rule-name-space-after',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondary, context) => {
const checker = whitespaceChecker('space', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'always-single-line'],
});
if (!validOptions) {
return;
}
atRuleNameSpaceChecker({
root,
result,
locationChecker: checker.after,
checkedRuleName: ruleName,
fix: context.fix
? (atRule) => {
if (typeof atRule.raws.afterName === 'string') {
atRule.raws.afterName = atRule.raws.afterName.replace(/^\s*/, ' ');
}
}
: null,
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,74 @@
'use strict';
const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
const { atKeywords } = require('../../reference/atKeywords');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const vendor = require('../../utils/vendor');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'at-rule-no-unknown';
const messages = ruleMessages(ruleName, {
rejected: (atRule) => `Unexpected unknown at-rule "${atRule}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/at-rule-no-unknown',
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{ actual: primary },
{
actual: secondaryOptions,
possible: {
ignoreAtRules: [isString, isRegExp],
},
optional: true,
},
);
if (!validOptions) {
return;
}
root.walkAtRules((atRule) => {
if (!isStandardSyntaxAtRule(atRule)) {
return;
}
const name = atRule.name;
// Return early if at-rule is to be ignored
if (optionsMatches(secondaryOptions, 'ignoreAtRules', atRule.name)) {
return;
}
if (vendor.prefix(name) || atKeywords.has(name.toLowerCase())) {
return;
}
const atName = `@${name}`;
report({
message: messages.rejected(atName),
node: atRule,
ruleName,
result,
word: atName,
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,64 @@
'use strict';
const isAutoprefixable = require('../../utils/isAutoprefixable');
const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'at-rule-no-vendor-prefix';
const messages = ruleMessages(ruleName, {
rejected: (p) => `Unexpected vendor-prefixed at-rule "@${p}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/at-rule-no-vendor-prefix',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondary, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual: primary });
if (!validOptions) {
return;
}
root.walkAtRules((atRule) => {
if (!isStandardSyntaxAtRule(atRule)) {
return;
}
const name = atRule.name;
if (!name.startsWith('-')) {
return;
}
if (!isAutoprefixable.atRuleName(name)) {
return;
}
if (context.fix) {
atRule.name = isAutoprefixable.unprefix(atRule.name);
return;
}
report({
message: messages.rejected(name),
node: atRule,
word: `@${name}`,
result,
ruleName,
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,73 @@
'use strict';
const flattenArray = require('../../utils/flattenArray');
const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateObjectWithArrayProps = require('../../utils/validateObjectWithArrayProps');
const validateOptions = require('../../utils/validateOptions');
const { isString } = require('../../utils/validateTypes');
const ruleName = 'at-rule-property-required-list';
const messages = ruleMessages(ruleName, {
expected: (property, atRule) => `Expected property "${property}" for at-rule "${atRule}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/at-rule-property-required-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;
}
root.walkAtRules((atRule) => {
if (!isStandardSyntaxAtRule(atRule)) {
return;
}
const { name, nodes } = atRule;
const atRuleName = name.toLowerCase();
const propList = flattenArray(primary[atRuleName]);
if (!propList) {
return;
}
for (const property of propList) {
const propertyName = property.toLowerCase();
const hasProperty = nodes.find(
(node) => node.type === 'decl' && node.prop.toLowerCase() === propertyName,
);
if (hasProperty) {
continue;
}
report({
message: messages.expected(propertyName, atRuleName),
node: atRule,
word: `@${atRule.name}`,
result,
ruleName,
});
continue;
}
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,83 @@
'use strict';
const hasBlock = require('../../utils/hasBlock');
const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
const nextNonCommentNode = require('../../utils/nextNonCommentNode');
const rawNodeString = require('../../utils/rawNodeString');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'at-rule-semicolon-newline-after';
const messages = ruleMessages(ruleName, {
expectedAfter: () => 'Expected newline after ";"',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/at-rule-semicolon-newline-after',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondary, context) => {
const checker = whitespaceChecker('newline', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always'],
});
if (!validOptions) {
return;
}
root.walkAtRules((atRule) => {
const nextNode = atRule.next();
if (!nextNode) {
return;
}
if (hasBlock(atRule)) {
return;
}
if (!isStandardSyntaxAtRule(atRule)) {
return;
}
// Allow an end-of-line comment
const nodeToCheck = nextNonCommentNode(nextNode);
if (!nodeToCheck) {
return;
}
checker.afterOneOnly({
source: rawNodeString(nodeToCheck),
index: -1,
err: (msg) => {
if (context.fix) {
nodeToCheck.raws.before = context.newline + nodeToCheck.raws.before;
} else {
report({
message: msg,
node: atRule,
index: atRule.toString().length + 1,
result,
ruleName,
});
}
},
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,67 @@
'use strict';
const hasBlock = require('../../utils/hasBlock');
const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
const rawNodeString = require('../../utils/rawNodeString');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'at-rule-semicolon-space-before';
const messages = ruleMessages(ruleName, {
expectedBefore: () => 'Expected single space before ";"',
rejectedBefore: () => 'Unexpected whitespace before ";"',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/at-rule-semicolon-space-before',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
const checker = whitespaceChecker('space', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'never'],
});
if (!validOptions) {
return;
}
root.walkAtRules((atRule) => {
if (hasBlock(atRule)) {
return;
}
if (!isStandardSyntaxAtRule(atRule)) {
return;
}
const nodeString = rawNodeString(atRule);
checker.before({
source: nodeString,
index: nodeString.length,
err: (m) => {
report({
message: m,
node: atRule,
index: nodeString.length - 1,
result,
ruleName,
});
},
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,55 @@
'use strict';
const isStandardSyntaxAtRule = require('../utils/isStandardSyntaxAtRule');
const report = require('../utils/report');
/**
* @param {{
* root: import('postcss').Root,
* locationChecker: (opts: { source: string, index: number, err: (msg: string) => void, errTarget: string }) => void,
* result: import('stylelint').PostcssResult,
* checkedRuleName: string,
* fix?: ((atRule: import('postcss').AtRule) => void) | null,
* }} options
*/
module.exports = function atRuleNameSpaceChecker(options) {
options.root.walkAtRules((atRule) => {
if (!isStandardSyntaxAtRule(atRule)) {
return;
}
checkColon(
`@${atRule.name}${atRule.raws.afterName || ''}${atRule.params}`,
atRule.name.length,
atRule,
);
});
/**
* @param {string} source
* @param {number} index
* @param {import('postcss').AtRule} node
*/
function checkColon(source, index, node) {
options.locationChecker({
source,
index,
err: (m) => {
if (options.fix) {
options.fix(node);
return;
}
report({
message: m,
node,
index,
result: options.result,
ruleName: options.checkedRuleName,
});
},
errTarget: `@${node.name}`,
});
}
};

View File

@@ -0,0 +1,128 @@
'use strict';
const addEmptyLineAfter = require('../../utils/addEmptyLineAfter');
const blockString = require('../../utils/blockString');
const hasBlock = require('../../utils/hasBlock');
const hasEmptyBlock = require('../../utils/hasEmptyBlock');
const hasEmptyLine = require('../../utils/hasEmptyLine');
const isSingleLineString = require('../../utils/isSingleLineString');
const optionsMatches = require('../../utils/optionsMatches');
const removeEmptyLinesAfter = require('../../utils/removeEmptyLinesAfter');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'block-closing-brace-empty-line-before';
const messages = ruleMessages(ruleName, {
expected: 'Expected empty line before closing brace',
rejected: 'Unexpected empty line before closing brace',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/block-closing-brace-empty-line-before',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: ['always-multi-line', 'never'],
},
{
actual: secondaryOptions,
possible: {
except: ['after-closing-brace'],
},
optional: true,
},
);
if (!validOptions) {
return;
}
// Check both kinds of statements: rules and at-rules
root.walkRules(check);
root.walkAtRules(check);
/**
* @param {import('postcss').Rule | import('postcss').AtRule} statement
*/
function check(statement) {
// Return early if blockless or has empty block
if (!hasBlock(statement) || hasEmptyBlock(statement)) {
return;
}
// Get whitespace after ""}", ignoring extra semicolon
const before = (statement.raws.after || '').replace(/;+/, '');
// Calculate index
const statementString = statement.toString();
let index = statementString.length - 1;
if (statementString[index - 1] === '\r') {
index -= 1;
}
// Set expectation
const expectEmptyLineBefore = (() => {
const childNodeTypes = statement.nodes.map((item) => item.type);
// Reverse the primary options if `after-closing-brace` is set
if (
optionsMatches(secondaryOptions, 'except', 'after-closing-brace') &&
statement.type === 'atrule' &&
!childNodeTypes.includes('decl')
) {
return primary === 'never';
}
return primary === 'always-multi-line' && !isSingleLineString(blockString(statement));
})();
// Check for at least one empty line
const hasEmptyLineBefore = hasEmptyLine(before);
// Return if the expectation is met
if (expectEmptyLineBefore === hasEmptyLineBefore) {
return;
}
if (context.fix) {
const { newline } = context;
if (typeof newline !== 'string') return;
if (expectEmptyLineBefore) {
addEmptyLineAfter(statement, newline);
} else {
removeEmptyLinesAfter(statement, newline);
}
return;
}
const message = expectEmptyLineBefore ? messages.expected : messages.rejected;
report({
message,
result,
ruleName,
node: statement,
index,
});
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,151 @@
'use strict';
const blockString = require('../../utils/blockString');
const hasBlock = require('../../utils/hasBlock');
const optionsMatches = require('../../utils/optionsMatches');
const rawNodeString = require('../../utils/rawNodeString');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const { isString } = require('../../utils/validateTypes');
const ruleName = 'block-closing-brace-newline-after';
const messages = ruleMessages(ruleName, {
expectedAfter: () => 'Expected newline after "}"',
expectedAfterSingleLine: () => 'Expected newline after "}" of a single-line block',
rejectedAfterSingleLine: () => 'Unexpected whitespace after "}" of a single-line block',
expectedAfterMultiLine: () => 'Expected newline after "}" of a multi-line block',
rejectedAfterMultiLine: () => 'Unexpected whitespace after "}" of a multi-line block',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/block-closing-brace-newline-after',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
const checker = whitespaceChecker('newline', primary, messages);
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: [
'always',
'always-single-line',
'never-single-line',
'always-multi-line',
'never-multi-line',
],
},
{
actual: secondaryOptions,
possible: {
ignoreAtRules: [isString],
},
optional: true,
},
);
if (!validOptions) {
return;
}
// Check both kinds of statements: rules and at-rules
root.walkRules(check);
root.walkAtRules(check);
/**
* @param {import('postcss').Rule | import('postcss').AtRule} statement
*/
function check(statement) {
if (!hasBlock(statement)) {
return;
}
if (
statement.type === 'atrule' &&
optionsMatches(secondaryOptions, 'ignoreAtRules', statement.name)
) {
return;
}
const nextNode = statement.next();
if (!nextNode) {
return;
}
// Allow an end-of-line comment x spaces after the brace
const nextNodeIsSingleLineComment =
nextNode.type === 'comment' &&
!/[^ ]/.test(nextNode.raws.before || '') &&
!nextNode.toString().includes('\n');
const nodeToCheck = nextNodeIsSingleLineComment ? nextNode.next() : nextNode;
if (!nodeToCheck) {
return;
}
let reportIndex = statement.toString().length;
let source = rawNodeString(nodeToCheck);
// Skip a semicolon at the beginning, if any
if (source && source.startsWith(';')) {
source = source.slice(1);
reportIndex++;
}
// Only check one after, because there might be other
// spaces handled by the indentation rule
checker.afterOneOnly({
source,
index: -1,
lineCheckStr: blockString(statement),
err: (msg) => {
if (context.fix) {
const nodeToCheckRaws = nodeToCheck.raws;
if (typeof nodeToCheckRaws.before !== 'string') return;
if (primary.startsWith('always')) {
const index = nodeToCheckRaws.before.search(/\r?\n/);
nodeToCheckRaws.before =
index >= 0
? nodeToCheckRaws.before.slice(index)
: context.newline + nodeToCheckRaws.before;
return;
}
if (primary.startsWith('never')) {
nodeToCheckRaws.before = '';
return;
}
}
report({
message: msg,
node: statement,
index: reportIndex,
result,
ruleName,
});
},
});
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,131 @@
'use strict';
const blockString = require('../../utils/blockString');
const hasBlock = require('../../utils/hasBlock');
const hasEmptyBlock = require('../../utils/hasEmptyBlock');
const isSingleLineString = require('../../utils/isSingleLineString');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'block-closing-brace-newline-before';
const messages = ruleMessages(ruleName, {
expectedBefore: 'Expected newline before "}"',
expectedBeforeMultiLine: 'Expected newline before "}" of a multi-line block',
rejectedBeforeMultiLine: 'Unexpected whitespace before "}" of a multi-line block',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/block-closing-brace-newline-before',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'always-multi-line', 'never-multi-line'],
});
if (!validOptions) {
return;
}
// Check both kinds of statements: rules and at-rules
root.walkRules(check);
root.walkAtRules(check);
/**
* @param {import('postcss').Rule | import('postcss').AtRule} statement
*/
function check(statement) {
// Return early if blockless or has empty block
if (!hasBlock(statement) || hasEmptyBlock(statement)) {
return;
}
// Ignore extra semicolon
const after = (statement.raws.after || '').replace(/;+/, '');
if (after === undefined) {
return;
}
const blockIsMultiLine = !isSingleLineString(blockString(statement));
const statementString = statement.toString();
let index = statementString.length - 2;
if (statementString[index - 1] === '\r') {
index -= 1;
}
// We're really just checking whether a
// newline *starts* the block's final space -- between
// the last declaration and the closing brace. We can
// ignore any other whitespace between them, because that
// will be checked by the indentation rule.
if (!after.startsWith('\n') && !after.startsWith('\r\n')) {
if (primary === 'always') {
complain(messages.expectedBefore);
} else if (blockIsMultiLine && primary === 'always-multi-line') {
complain(messages.expectedBeforeMultiLine);
}
}
if (after !== '' && blockIsMultiLine && primary === 'never-multi-line') {
complain(messages.rejectedBeforeMultiLine);
}
/**
* @param {string} message
*/
function complain(message) {
if (context.fix) {
const statementRaws = statement.raws;
if (typeof statementRaws.after !== 'string') return;
if (primary.startsWith('always')) {
const firstWhitespaceIndex = statementRaws.after.search(/\s/);
const newlineBefore =
firstWhitespaceIndex >= 0
? statementRaws.after.slice(0, firstWhitespaceIndex)
: statementRaws.after;
const newlineAfter =
firstWhitespaceIndex >= 0 ? statementRaws.after.slice(firstWhitespaceIndex) : '';
const newlineIndex = newlineAfter.search(/\r?\n/);
statementRaws.after =
newlineIndex >= 0
? newlineBefore + newlineAfter.slice(newlineIndex)
: newlineBefore + context.newline + newlineAfter;
return;
}
if (primary === 'never-multi-line') {
statementRaws.after = statementRaws.after.replace(/\s/g, '');
return;
}
}
report({
message,
result,
ruleName,
node: statement,
index,
});
}
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,95 @@
'use strict';
const blockString = require('../../utils/blockString');
const hasBlock = require('../../utils/hasBlock');
const rawNodeString = require('../../utils/rawNodeString');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'block-closing-brace-space-after';
const messages = ruleMessages(ruleName, {
expectedAfter: () => 'Expected single space after "}"',
rejectedAfter: () => 'Unexpected whitespace after "}"',
expectedAfterSingleLine: () => 'Expected single space after "}" of a single-line block',
rejectedAfterSingleLine: () => 'Unexpected whitespace after "}" of a single-line block',
expectedAfterMultiLine: () => 'Expected single space after "}" of a multi-line block',
rejectedAfterMultiLine: () => 'Unexpected whitespace after "}" of a multi-line block',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/block-closing-brace-space-after',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
const checker = whitespaceChecker('space', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [
'always',
'never',
'always-single-line',
'never-single-line',
'always-multi-line',
'never-multi-line',
],
});
if (!validOptions) {
return;
}
// Check both kinds of statements: rules and at-rules
root.walkRules(check);
root.walkAtRules(check);
/**
* @param {import('postcss').Rule | import('postcss').AtRule} statement
*/
function check(statement) {
const nextNode = statement.next();
if (!nextNode) {
return;
}
if (!hasBlock(statement)) {
return;
}
let reportIndex = statement.toString().length;
let source = rawNodeString(nextNode);
// Skip a semicolon at the beginning, if any
if (source && source.startsWith(';')) {
source = source.slice(1);
reportIndex++;
}
checker.after({
source,
index: -1,
lineCheckStr: blockString(statement),
err: (msg) => {
report({
message: msg,
node: statement,
index: reportIndex,
result,
ruleName,
});
},
});
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,108 @@
'use strict';
const blockString = require('../../utils/blockString');
const hasBlock = require('../../utils/hasBlock');
const hasEmptyBlock = require('../../utils/hasEmptyBlock');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'block-closing-brace-space-before';
const messages = ruleMessages(ruleName, {
expectedBefore: () => 'Expected single space before "}"',
rejectedBefore: () => 'Unexpected whitespace before "}"',
expectedBeforeSingleLine: () => 'Expected single space before "}" of a single-line block',
rejectedBeforeSingleLine: () => 'Unexpected whitespace before "}" of a single-line block',
expectedBeforeMultiLine: () => 'Expected single space before "}" of a multi-line block',
rejectedBeforeMultiLine: () => 'Unexpected whitespace before "}" of a multi-line block',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/block-closing-brace-space-before',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
const checker = whitespaceChecker('space', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [
'always',
'never',
'always-single-line',
'never-single-line',
'always-multi-line',
'never-multi-line',
],
});
if (!validOptions) {
return;
}
// Check both kinds of statement: rules and at-rules
root.walkRules(check);
root.walkAtRules(check);
/**
* @param {import('postcss').Rule | import('postcss').AtRule} statement
*/
function check(statement) {
// Return early if blockless or has empty block
if (!hasBlock(statement) || hasEmptyBlock(statement)) {
return;
}
const source = blockString(statement);
const statementString = statement.toString();
let index = statementString.length - 2;
if (statementString[index - 1] === '\r') {
index -= 1;
}
checker.before({
source,
index: source.length - 1,
err: (msg) => {
if (context.fix) {
const statementRaws = statement.raws;
if (typeof statementRaws.after !== 'string') return;
if (primary.startsWith('always')) {
statementRaws.after = statementRaws.after.replace(/\s*$/, ' ');
return;
}
if (primary.startsWith('never')) {
statementRaws.after = statementRaws.after.replace(/\s*$/, '');
return;
}
}
report({
message: msg,
node: statement,
index,
result,
ruleName,
});
},
});
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,103 @@
'use strict';
const beforeBlockString = require('../../utils/beforeBlockString');
const hasBlock = require('../../utils/hasBlock');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const { isStylelintCommand } = require('../../utils/stylelintCommand');
const { isComment } = require('../../utils/typeGuards');
const validateOptions = require('../../utils/validateOptions');
const { isBoolean } = require('../../utils/validateTypes');
const ruleName = 'block-no-empty';
const messages = ruleMessages(ruleName, {
rejected: 'Unexpected empty block',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/block-no-empty',
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: isBoolean,
},
{
actual: secondaryOptions,
possible: {
ignore: ['comments'],
},
optional: true,
},
);
if (!validOptions) {
return;
}
const ignoreComments = optionsMatches(secondaryOptions, 'ignore', 'comments');
// Check both kinds of statements: rules and at-rules
root.walkRules(check);
root.walkAtRules(check);
/** @typedef {import('postcss').Rule | import('postcss').AtRule} Statement */
/**
* @param {Statement} statement
*/
function check(statement) {
if (!hasBlock(statement)) {
return;
}
if (hasNotableChild(statement)) {
return;
}
let index = beforeBlockString(statement, { noRawBefore: true }).length;
// For empty blocks when using SugarSS parser
if (statement.raws.between === undefined) {
index--;
}
report({
message: messages.rejected,
node: statement,
start: statement.positionBy({ index }),
result,
ruleName,
});
}
/**
* @param {Statement} statement
* @returns {boolean}
*/
function hasNotableChild(statement) {
return statement.nodes.some((child) => {
if (isComment(child)) {
if (ignoreComments) return false;
if (isStylelintCommand(child)) return false;
}
return true;
});
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,181 @@
'use strict';
const beforeBlockString = require('../../utils/beforeBlockString');
const blockString = require('../../utils/blockString');
const hasBlock = require('../../utils/hasBlock');
const hasEmptyBlock = require('../../utils/hasEmptyBlock');
const optionsMatches = require('../../utils/optionsMatches');
const rawNodeString = require('../../utils/rawNodeString');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'block-opening-brace-newline-after';
const messages = ruleMessages(ruleName, {
expectedAfter: () => 'Expected newline after "{"',
expectedAfterMultiLine: () => 'Expected newline after "{" of a multi-line block',
rejectedAfterMultiLine: () => 'Unexpected whitespace after "{" of a multi-line block',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/block-opening-brace-newline-after',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
const checker = whitespaceChecker('newline', primary, messages);
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: ['always', 'rules', 'always-multi-line', 'never-multi-line'],
},
{
actual: secondaryOptions,
possible: {
ignore: ['rules'],
},
optional: true,
},
);
if (!validOptions) {
return;
}
// Check both kinds of statement: rules and at-rules
if (!optionsMatches(secondaryOptions, 'ignore', 'rules')) {
root.walkRules(check);
}
root.walkAtRules(check);
/**
* @param {import('postcss').Rule | import('postcss').AtRule} statement
*/
function check(statement) {
// Return early if blockless or has an empty block
if (!hasBlock(statement) || hasEmptyBlock(statement)) {
return;
}
const backupCommentNextBefores = new Map();
/**
* next node with checking newlines after comment
*
* @param {import('postcss').ChildNode | undefined} startNode
* @returns {import('postcss').ChildNode | undefined}
*/
function nextNode(startNode) {
if (!startNode || !startNode.next) return;
if (startNode.type === 'comment') {
const reNewLine = /\r?\n/;
const newLineMatch = reNewLine.test(startNode.raws.before || '');
const next = startNode.next();
if (next && newLineMatch && !reNewLine.test(next.raws.before || '')) {
backupCommentNextBefores.set(next, next.raws.before);
next.raws.before = startNode.raws.before;
}
return nextNode(next);
}
return startNode;
}
// Allow an end-of-line comment
const nodeToCheck = nextNode(statement.first);
if (!nodeToCheck) {
return;
}
checker.afterOneOnly({
source: rawNodeString(nodeToCheck),
index: -1,
lineCheckStr: blockString(statement),
err: (m) => {
if (context.fix) {
const nodeToCheckRaws = nodeToCheck.raws;
if (typeof nodeToCheckRaws.before !== 'string') return;
if (primary.startsWith('always')) {
const index = nodeToCheckRaws.before.search(/\r?\n/);
nodeToCheckRaws.before =
index >= 0
? nodeToCheckRaws.before.slice(index)
: context.newline + nodeToCheckRaws.before;
backupCommentNextBefores.delete(nodeToCheck);
return;
}
if (primary === 'never-multi-line') {
// Restore the `before` of the node next to the comment node.
for (const [node, before] of backupCommentNextBefores.entries()) {
node.raws.before = before;
}
backupCommentNextBefores.clear();
// Fix
const reNewLine = /\r?\n/;
let fixTarget = statement.first;
while (fixTarget) {
const fixTargetRaws = fixTarget.raws;
if (typeof fixTargetRaws.before !== 'string') continue;
if (reNewLine.test(fixTargetRaws.before || '')) {
fixTargetRaws.before = fixTargetRaws.before.replace(/\r?\n/g, '');
}
if (fixTarget.type !== 'comment') {
break;
}
fixTarget = fixTarget.next();
}
nodeToCheckRaws.before = '';
return;
}
}
report({
message: m,
node: statement,
index: beforeBlockString(statement, { noRawBefore: true }).length + 1,
result,
ruleName,
});
},
});
// Restore the `before` of the node next to the comment node.
for (const [node, before] of backupCommentNextBefores.entries()) {
node.raws.before = before;
}
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,119 @@
'use strict';
const beforeBlockString = require('../../utils/beforeBlockString');
const blockString = require('../../utils/blockString');
const hasBlock = require('../../utils/hasBlock');
const hasEmptyBlock = require('../../utils/hasEmptyBlock');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'block-opening-brace-newline-before';
const messages = ruleMessages(ruleName, {
expectedBefore: () => 'Expected newline before "{"',
expectedBeforeSingleLine: () => 'Expected newline before "{" of a single-line block',
rejectedBeforeSingleLine: () => 'Unexpected whitespace before "{" of a single-line block',
expectedBeforeMultiLine: () => 'Expected newline before "{" of a multi-line block',
rejectedBeforeMultiLine: () => 'Unexpected whitespace before "{" of a multi-line block',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/block-opening-brace-newline-before',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
const checker = whitespaceChecker('newline', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [
'always',
'always-single-line',
'never-single-line',
'always-multi-line',
'never-multi-line',
],
});
if (!validOptions) {
return;
}
// Check both kinds of statement: rules and at-rules
root.walkRules(check);
root.walkAtRules(check);
/**
* @param {import('postcss').Rule | import('postcss').AtRule} statement
*/
function check(statement) {
// Return early if blockless or has an empty block
if (!hasBlock(statement) || hasEmptyBlock(statement)) {
return;
}
const source = beforeBlockString(statement);
const beforeBraceNoRaw = beforeBlockString(statement, {
noRawBefore: true,
});
let index = beforeBraceNoRaw.length - 1;
if (beforeBraceNoRaw[index - 1] === '\r') {
index -= 1;
}
checker.beforeAllowingIndentation({
lineCheckStr: blockString(statement),
source,
index: source.length,
err: (m) => {
if (context.fix) {
const statementRaws = statement.raws;
if (typeof statementRaws.between !== 'string') return;
if (primary.startsWith('always')) {
const spaceIndex = statementRaws.between.search(/\s+$/);
if (spaceIndex >= 0) {
statement.raws.between =
statementRaws.between.slice(0, spaceIndex) +
context.newline +
statementRaws.between.slice(spaceIndex);
} else {
statementRaws.between += context.newline;
}
return;
}
if (primary.startsWith('never')) {
statementRaws.between = statementRaws.between.replace(/\s*$/, '');
return;
}
}
report({
message: m,
node: statement,
index,
result,
ruleName,
});
},
});
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,115 @@
'use strict';
const beforeBlockString = require('../../utils/beforeBlockString');
const blockString = require('../../utils/blockString');
const hasBlock = require('../../utils/hasBlock');
const hasEmptyBlock = require('../../utils/hasEmptyBlock');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'block-opening-brace-space-after';
const messages = ruleMessages(ruleName, {
expectedAfter: () => 'Expected single space after "{"',
rejectedAfter: () => 'Unexpected whitespace after "{"',
expectedAfterSingleLine: () => 'Expected single space after "{" of a single-line block',
rejectedAfterSingleLine: () => 'Unexpected whitespace after "{" of a single-line block',
expectedAfterMultiLine: () => 'Expected single space after "{" of a multi-line block',
rejectedAfterMultiLine: () => 'Unexpected whitespace after "{" of a multi-line block',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/block-opening-brace-space-after',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
const checker = whitespaceChecker('space', primary, messages);
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: [
'always',
'never',
'always-single-line',
'never-single-line',
'always-multi-line',
'never-multi-line',
],
},
{
actual: secondaryOptions,
possible: {
ignore: ['at-rules'],
},
optional: true,
},
);
if (!validOptions) {
return;
}
// Check both kinds of statements: rules and at-rules
root.walkRules(check);
if (!optionsMatches(secondaryOptions, 'ignore', 'at-rules')) {
root.walkAtRules(check);
}
/**
* @param {import('postcss').Rule | import('postcss').AtRule} statement
*/
function check(statement) {
// Return early if blockless or has an empty block
if (!hasBlock(statement) || hasEmptyBlock(statement)) {
return;
}
checker.after({
source: blockString(statement),
index: 0,
err: (m) => {
if (context.fix) {
const statementFirst = statement.first;
if (statementFirst == null) return;
if (primary.startsWith('always')) {
statementFirst.raws.before = ' ';
return;
}
if (primary.startsWith('never')) {
statementFirst.raws.before = '';
return;
}
}
report({
message: m,
node: statement,
index: beforeBlockString(statement, { noRawBefore: true }).length + 1,
result,
ruleName,
});
},
});
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,138 @@
'use strict';
const beforeBlockString = require('../../utils/beforeBlockString');
const blockString = require('../../utils/blockString');
const hasBlock = require('../../utils/hasBlock');
const hasEmptyBlock = require('../../utils/hasEmptyBlock');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'block-opening-brace-space-before';
const messages = ruleMessages(ruleName, {
expectedBefore: () => 'Expected single space before "{"',
rejectedBefore: () => 'Unexpected whitespace before "{"',
expectedBeforeSingleLine: () => 'Expected single space before "{" of a single-line block',
rejectedBeforeSingleLine: () => 'Unexpected whitespace before "{" of a single-line block',
expectedBeforeMultiLine: () => 'Expected single space before "{" of a multi-line block',
rejectedBeforeMultiLine: () => 'Unexpected whitespace before "{" of a multi-line block',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/block-opening-brace-space-before',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
const checker = whitespaceChecker('space', primary, messages);
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: [
'always',
'never',
'always-single-line',
'never-single-line',
'always-multi-line',
'never-multi-line',
],
},
{
actual: secondaryOptions,
possible: {
ignoreAtRules: [isString, isRegExp],
ignoreSelectors: [isString, isRegExp],
},
optional: true,
},
);
if (!validOptions) {
return;
}
// Check both kinds of statements: rules and at-rules
root.walkRules(check);
root.walkAtRules(check);
/**
* @param {import('postcss').Rule | import('postcss').AtRule} statement
*/
function check(statement) {
// Return early if blockless or has an empty block
if (!hasBlock(statement) || hasEmptyBlock(statement)) {
return;
}
// Return early if at-rule is to be ignored
if (
statement.type === 'atrule' &&
optionsMatches(secondaryOptions, 'ignoreAtRules', statement.name)
) {
return;
}
// Return early if selector is to be ignored
if (
statement.type === 'rule' &&
optionsMatches(secondaryOptions, 'ignoreSelectors', statement.selector)
) {
return;
}
const source = beforeBlockString(statement);
const beforeBraceNoRaw = beforeBlockString(statement, {
noRawBefore: true,
});
let index = beforeBraceNoRaw.length - 1;
if (beforeBraceNoRaw[index - 1] === '\r') {
index -= 1;
}
checker.before({
source,
index: source.length,
lineCheckStr: blockString(statement),
err: (m) => {
if (context.fix) {
if (primary.startsWith('always')) {
statement.raws.between = ' ';
return;
}
if (primary.startsWith('never')) {
statement.raws.between = '';
return;
}
}
report({
message: m,
node: statement,
index,
result,
ruleName,
});
},
});
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,132 @@
'use strict';
const valueParser = require('postcss-value-parser');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const isStandardSyntaxColorFunction = require('../../utils/isStandardSyntaxColorFunction');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const setDeclarationValue = require('../../utils/setDeclarationValue');
const { isValueFunction } = require('../../utils/typeGuards');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'color-function-notation';
const messages = ruleMessages(ruleName, {
expected: (primary) => `Expected ${primary} color-function notation`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/color-function-notation',
fixable: true,
};
const LEGACY_FUNCS = new Set(['rgba', 'hsla']);
const LEGACY_NOTATION_FUNCS = new Set(['rgb', 'rgba', 'hsl', 'hsla']);
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['modern', 'legacy'],
});
if (!validOptions) return;
root.walkDecls((decl) => {
let needsFix = false;
const parsedValue = valueParser(getDeclarationValue(decl));
parsedValue.walk((node) => {
if (!isValueFunction(node)) return;
if (!isStandardSyntaxColorFunction(node)) return;
const { value, sourceIndex, sourceEndIndex, nodes } = node;
if (!LEGACY_NOTATION_FUNCS.has(value.toLowerCase())) return;
if (primary === 'modern' && !hasCommas(node)) return;
if (primary === 'legacy' && hasCommas(node)) return;
if (context.fix && primary === 'modern') {
let commaCount = 0;
// Convert punctuation
node.nodes = nodes.map((childNode) => {
if (isComma(childNode)) {
// Non-alpha commas to space and alpha commas to slashes
if (commaCount < 2) {
// @ts-expect-error -- TS2322: Type '"space"' is not assignable to type '"div"'.
childNode.type = 'space';
childNode.value = atLeastOneSpace(childNode.after);
commaCount++;
} else {
childNode.value = '/';
childNode.before = atLeastOneSpace(childNode.before);
childNode.after = atLeastOneSpace(childNode.after);
}
}
return childNode;
});
// Remove trailing 'a' from legacy function name
if (LEGACY_FUNCS.has(node.value.toLowerCase())) {
node.value = node.value.slice(0, -1);
}
needsFix = true;
return;
}
const index = declarationValueIndex(decl) + sourceIndex;
const endIndex = index + (sourceEndIndex - sourceIndex);
report({
message: messages.expected(primary),
node: decl,
index,
endIndex,
result,
ruleName,
});
});
if (needsFix) {
setDeclarationValue(decl, parsedValue.toString());
}
});
};
};
/**
* @param {string} whitespace
*/
function atLeastOneSpace(whitespace) {
return whitespace !== '' ? whitespace : ' ';
}
/**
* @param {import('postcss-value-parser').Node} node
* @returns {node is import('postcss-value-parser').DivNode}
*/
function isComma(node) {
return node.type === 'div' && node.value === ',';
}
/**
* @param {import('postcss-value-parser').FunctionNode} node
*/
function hasCommas(node) {
return node.nodes && node.nodes.some((childNode) => isComma(childNode));
}
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,86 @@
'use strict';
const declarationValueIndex = require('../../utils/declarationValueIndex');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const valueParser = require('postcss-value-parser');
const ruleName = 'color-hex-alpha';
const messages = ruleMessages(ruleName, {
expected: (hex) => `Expected alpha channel in "${hex}"`,
unexpected: (hex) => `Unexpected alpha channel in "${hex}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/color-hex-alpha',
};
const HEX = /^#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i;
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'never'],
});
if (!validOptions) return;
root.walkDecls((decl) => {
const parsedValue = valueParser(decl.value);
parsedValue.walk((node) => {
if (isUrlFunction(node)) return false;
if (!isHexColor(node)) return;
const { value } = node;
if (primary === 'always' && hasAlphaChannel(value)) return;
if (primary === 'never' && !hasAlphaChannel(value)) return;
const index = declarationValueIndex(decl) + node.sourceIndex;
const endIndex = index + value.length;
report({
message: primary === 'never' ? messages.unexpected(value) : messages.expected(value),
node: decl,
index,
endIndex,
result,
ruleName,
});
});
});
};
};
/**
* @param {import('postcss-value-parser').Node} node
*/
function isUrlFunction({ type, value }) {
return type === 'function' && value === 'url';
}
/**
* @param {import('postcss-value-parser').Node} node
*/
function isHexColor({ type, value }) {
return type === 'word' && HEX.test(value);
}
/**
* @param {string} hex
*/
function hasAlphaChannel(hex) {
return hex.length === 5 || hex.length === 9;
}
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,93 @@
'use strict';
const valueParser = require('postcss-value-parser');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const setDeclarationValue = require('../../utils/setDeclarationValue');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'color-hex-case';
const messages = ruleMessages(ruleName, {
expected: (actual, expected) => `Expected "${actual}" to be "${expected}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/color-hex-case',
fixable: true,
};
const HEX = /^#[0-9A-Za-z]+/;
const IGNORED_FUNCTIONS = new Set(['url']);
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['lower', 'upper'],
});
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
const parsedValue = valueParser(getDeclarationValue(decl));
let needsFix = false;
parsedValue.walk((node) => {
const { value } = node;
if (isIgnoredFunction(node)) return false;
if (!isHexColor(node)) return;
const expected = primary === 'lower' ? value.toLowerCase() : value.toUpperCase();
if (value === expected) return;
if (context.fix) {
node.value = expected;
needsFix = true;
return;
}
report({
message: messages.expected(value, expected),
node: decl,
index: declarationValueIndex(decl) + node.sourceIndex,
result,
ruleName,
});
});
if (needsFix) {
setDeclarationValue(decl, parsedValue.toString());
}
});
};
};
/**
* @param {import('postcss-value-parser').Node} node
*/
function isIgnoredFunction({ type, value }) {
return type === 'function' && IGNORED_FUNCTIONS.has(value.toLowerCase());
}
/**
* @param {import('postcss-value-parser').Node} node
*/
function isHexColor({ type, value }) {
return type === 'word' && HEX.test(value);
}
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,144 @@
'use strict';
const valueParser = require('postcss-value-parser');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const setDeclarationValue = require('../../utils/setDeclarationValue');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'color-hex-length';
const messages = ruleMessages(ruleName, {
expected: (actual, expected) => `Expected "${actual}" to be "${expected}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/color-hex-length',
fixable: true,
};
const HEX = /^#[0-9A-Za-z]+/;
const IGNORED_FUNCTIONS = new Set(['url']);
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['short', 'long'],
});
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
const parsedValue = valueParser(getDeclarationValue(decl));
let needsFix = false;
parsedValue.walk((node) => {
const { value: hexValue } = node;
if (isIgnoredFunction(node)) return false;
if (!isHexColor(node)) return;
if (primary === 'long' && hexValue.length !== 4 && hexValue.length !== 5) {
return;
}
if (primary === 'short' && (hexValue.length < 6 || !canShrink(hexValue))) {
return;
}
const variant = primary === 'long' ? longer : shorter;
const expectedHex = variant(hexValue);
if (context.fix) {
node.value = expectedHex;
needsFix = true;
return;
}
const index = declarationValueIndex(decl) + node.sourceIndex;
const endIndex = index + node.value.length;
report({
message: messages.expected(hexValue, expectedHex),
node: decl,
index,
endIndex,
result,
ruleName,
});
});
if (needsFix) {
setDeclarationValue(decl, parsedValue.toString());
}
});
};
};
/**
* @param {string} hex
*/
function canShrink(hex) {
hex = hex.toLowerCase();
return (
hex[1] === hex[2] &&
hex[3] === hex[4] &&
hex[5] === hex[6] &&
(hex.length === 7 || (hex.length === 9 && hex[7] === hex[8]))
);
}
/**
* @param {string} hex
*/
function shorter(hex) {
let hexVariant = '#';
for (let i = 1; i < hex.length; i += 2) {
hexVariant += hex[i];
}
return hexVariant;
}
/**
* @param {string} hex
*/
function longer(hex) {
let hexVariant = '#';
for (let i = 1; i < hex.length; i++) {
hexVariant += hex.charAt(i).repeat(2);
}
return hexVariant;
}
/**
* @param {import('postcss-value-parser').Node} node
*/
function isIgnoredFunction({ type, value }) {
return type === 'function' && IGNORED_FUNCTIONS.has(value.toLowerCase());
}
/**
* @param {import('postcss-value-parser').Node} node
*/
function isHexColor({ type, value }) {
return type === 'word' && HEX.test(value);
}
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,108 @@
const { colord, extend } = require('colord');
const valueParser = require('postcss-value-parser');
const namesPlugin = require('colord/plugins/names');
const hwbPlugin = require('colord/plugins/hwb');
const labPlugin = require('colord/plugins/lab');
const lchPlugin = require('colord/plugins/lch');
extend([
// Type definitions are not compatible with commonjs.
/** @type {any} */ (namesPlugin),
/** @type {any} */ (hwbPlugin),
/** @type {any} */ (labPlugin),
/** @type {any} */ (lchPlugin),
/* Syntaxes that is removed in Color Module Level 4 specification. */
// hwb() with comma
(_colordClass, parsers) => {
parsers.string.push([parseHwbWithCommaString, /** @type {any} */ ('hwb-with-comma')]);
},
// gray()
(_colordClass, parsers) => {
parsers.string.push([parseGrayString, /** @type {any} */ ('gray')]);
},
]);
module.exports = {
colord,
};
/**
* Parses a valid hwb with comma CSS color function
* https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hwb()#syntax
* @type {import('colord/types').ParseFunction<string>}
*/
function parseHwbWithCommaString(input) {
input = input.toLowerCase();
if (!input.startsWith('hwb(') || !input.endsWith(')') || input.includes('/')) {
return null;
}
const [hue, whiteness = '', blackness = '', alpha, ...extraArgs] = input.slice(4, -1).split(',');
if (!hue || !hue.trim() || !whiteness.trim() || !blackness.trim() || extraArgs.length > 0) {
return null;
}
// Change the delimiter and parse with colord.
const colordInstance = colord(
`hwb(${hue} ${whiteness} ${blackness}${alpha ? ` / ${alpha}` : ''})`,
);
if (!colordInstance.isValid()) {
return null;
}
return colordInstance.rgba;
}
/**
* Parses a valid gray() CSS color function
* @type {import('colord/types').ParseFunction<string>}
*/
function parseGrayString(input) {
input = input.toLowerCase();
if (!input.startsWith('gray(') || !input.endsWith(')')) {
return null;
}
const [lightness, alpha, ...extraArgs] = input.slice(5, -1).split(',');
if (!lightness || extraArgs.length > 0) {
return null;
}
const lightnessWithUnit = valueParser.unit(lightness.trim());
if (!lightnessWithUnit || !['', '%'].includes(lightnessWithUnit.unit)) {
return null;
}
/**
* @type {import('colord/types').LabColor | import('colord/types').LabaColor}
*/
let colorObject = {
l: Number(lightnessWithUnit.number),
a: 0,
b: 0,
};
if (alpha) {
const alphaWithUnit = valueParser.unit(alpha.trim());
if (!alphaWithUnit || !['', '%'].includes(alphaWithUnit.unit)) {
return null;
}
colorObject = {
...colorObject,
alpha: Number(alphaWithUnit.number) / (alphaWithUnit.unit ? 100 : 1),
};
}
return colord(colorObject).rgba;
}

165
node_modules/stylelint/lib/rules/color-named/index.js generated vendored Normal file
View File

@@ -0,0 +1,165 @@
'use strict';
const declarationValueIndex = require('../../utils/declarationValueIndex');
const isStandardSyntaxFunction = require('../../utils/isStandardSyntaxFunction');
const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
const optionsMatches = require('../../utils/optionsMatches');
const { acceptCustomIdentsProperties } = require('../../reference/properties');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const valueParser = require('postcss-value-parser');
const { isRegExp, isString } = require('../../utils/validateTypes');
const { colord } = require('./colordUtils');
const ruleName = 'color-named';
const messages = ruleMessages(ruleName, {
expected: (named, original) => `Expected "${original}" to be "${named}"`,
rejected: (named) => `Unexpected named color "${named}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/color-named',
};
// Todo tested on case insensitivity
const NODE_TYPES = new Set(['word', 'function']);
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: ['never', 'always-where-possible'],
},
{
actual: secondaryOptions,
possible: {
ignoreProperties: [isString, isRegExp],
ignore: ['inside-function'],
},
optional: true,
},
);
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
if (acceptCustomIdentsProperties.has(decl.prop)) {
return;
}
// Return early if the property is to be ignored
if (optionsMatches(secondaryOptions, 'ignoreProperties', decl.prop)) {
return;
}
valueParser(decl.value).walk((node) => {
const value = node.value;
const type = node.type;
const sourceIndex = node.sourceIndex;
if (optionsMatches(secondaryOptions, 'ignore', 'inside-function') && type === 'function') {
return false;
}
if (!isStandardSyntaxFunction(node)) {
return false;
}
if (!isStandardSyntaxValue(value)) {
return;
}
// Return early if neither a word nor a function
if (!NODE_TYPES.has(type)) {
return;
}
// Check for named colors for "never" option
if (
primary === 'never' &&
type === 'word' &&
/^[a-z]+$/iu.test(value) &&
value.toLowerCase() !== 'transparent' &&
colord(value).isValid()
) {
complain(
messages.rejected(value),
decl,
declarationValueIndex(decl) + sourceIndex,
value.length,
);
return;
}
// Check "always-where-possible" option ...
if (primary !== 'always-where-possible') {
return;
}
let rawColorString = null;
let colorString = null;
if (type === 'function') {
rawColorString = valueParser.stringify(node);
// First by checking for alternative color function representations ...
// Remove all spaces to match what's in `representations`
colorString = rawColorString.replace(/\s*([,/()])\s*/g, '$1').replace(/\s{2,}/g, ' ');
} else if (type === 'word' && value.startsWith('#')) {
// Then by checking for alternative hex representations
rawColorString = colorString = value;
} else {
return;
}
const color = colord(colorString);
if (!color.isValid()) {
return;
}
const namedColor = color.toName();
if (namedColor && namedColor.toLowerCase() !== 'transparent') {
complain(
messages.expected(namedColor, colorString),
decl,
declarationValueIndex(decl) + sourceIndex,
rawColorString.length,
);
}
});
});
/**
* @param {string} message
* @param {import('postcss').Node} node
* @param {number} index
* @param {number} length
*/
function complain(message, node, index, length) {
report({
result,
ruleName,
message,
node,
index,
endIndex: index + length,
});
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

75
node_modules/stylelint/lib/rules/color-no-hex/index.js generated vendored Normal file
View File

@@ -0,0 +1,75 @@
'use strict';
const valueParser = require('postcss-value-parser');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'color-no-hex';
const messages = ruleMessages(ruleName, {
rejected: (hex) => `Unexpected hex color "${hex}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/color-no-hex',
};
const HEX = /^#[0-9A-Za-z]+/;
const IGNORED_FUNCTIONS = new Set(['url']);
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual: primary });
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
const parsedValue = valueParser(getDeclarationValue(decl));
parsedValue.walk((node) => {
if (isIgnoredFunction(node)) return false;
if (!isHexColor(node)) return;
const index = declarationValueIndex(decl) + node.sourceIndex;
const endIndex = index + node.value.length;
report({
message: messages.rejected,
messageArgs: [node.value],
node: decl,
index,
endIndex,
result,
ruleName,
});
});
});
};
};
/**
* @param {import('postcss-value-parser').Node} node
*/
function isIgnoredFunction({ type, value }) {
return type === 'function' && IGNORED_FUNCTIONS.has(value.toLowerCase());
}
/**
* @param {import('postcss-value-parser').Node} node
*/
function isHexColor({ type, value }) {
return type === 'word' && HEX.test(value);
}
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,67 @@
'use strict';
const declarationValueIndex = require('../../utils/declarationValueIndex');
const isStandardSyntaxHexColor = require('../../utils/isStandardSyntaxHexColor');
const isValidHex = require('../../utils/isValidHex');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const valueParser = require('postcss-value-parser');
const ruleName = 'color-no-invalid-hex';
const messages = ruleMessages(ruleName, {
rejected: (hex) => `Unexpected invalid hex color "${hex}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/color-no-invalid-hex',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual: primary });
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
if (!isStandardSyntaxHexColor(decl.value)) {
return;
}
valueParser(decl.value).walk(({ value, type, sourceIndex }) => {
if (type === 'function' && value.endsWith('url')) return false;
if (type !== 'word') return;
const hexMatch = /^#[0-9A-Za-z]+/.exec(value);
if (!hexMatch) return;
const hexValue = hexMatch[0];
if (!hexValue || isValidHex(hexValue)) return;
const index = declarationValueIndex(decl) + sourceIndex;
const endIndex = index + hexValue.length;
report({
message: messages.rejected(hexValue),
node: decl,
index,
endIndex,
result,
ruleName,
});
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,134 @@
'use strict';
const addEmptyLineBefore = require('../../utils/addEmptyLineBefore');
const hasEmptyLine = require('../../utils/hasEmptyLine');
const isAfterComment = require('../../utils/isAfterComment');
const isFirstNested = require('../../utils/isFirstNested');
const isFirstNodeOfRoot = require('../../utils/isFirstNodeOfRoot');
const isSharedLineComment = require('../../utils/isSharedLineComment');
const isStandardSyntaxComment = require('../../utils/isStandardSyntaxComment');
const optionsMatches = require('../../utils/optionsMatches');
const removeEmptyLinesBefore = require('../../utils/removeEmptyLinesBefore');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'comment-empty-line-before';
const messages = ruleMessages(ruleName, {
expected: 'Expected empty line before comment',
rejected: 'Unexpected empty line before comment',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/comment-empty-line-before',
fixable: true,
};
const stylelintCommandPrefix = 'stylelint-';
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: ['always', 'never'],
},
{
actual: secondaryOptions,
possible: {
except: ['first-nested'],
ignore: ['stylelint-commands', 'after-comment'],
ignoreComments: [isString, isRegExp],
},
optional: true,
},
);
if (!validOptions) {
return;
}
root.walkComments((comment) => {
// Ignore the first node
if (isFirstNodeOfRoot(comment)) {
return;
}
// Optionally ignore stylelint commands
if (
comment.text.startsWith(stylelintCommandPrefix) &&
optionsMatches(secondaryOptions, 'ignore', 'stylelint-commands')
) {
return;
}
// Optionally ignore newlines between comments
if (optionsMatches(secondaryOptions, 'ignore', 'after-comment') && isAfterComment(comment)) {
return;
}
// Ignore comments matching the ignoreComments option.
if (optionsMatches(secondaryOptions, 'ignoreComments', comment.text)) {
return;
}
// Ignore shared-line comments
if (isSharedLineComment(comment)) {
return;
}
// Ignore non-standard comments
if (!isStandardSyntaxComment(comment)) {
return;
}
const expectEmptyLineBefore = (() => {
if (optionsMatches(secondaryOptions, 'except', 'first-nested') && isFirstNested(comment)) {
return false;
}
return primary === 'always';
})();
const before = comment.raws.before || '';
const hasEmptyLineBefore = hasEmptyLine(before);
// Return if the expectation is met
if (expectEmptyLineBefore === hasEmptyLineBefore) {
return;
}
// Fix
if (context.fix) {
if (typeof context.newline !== 'string') return;
if (expectEmptyLineBefore) {
addEmptyLineBefore(comment, context.newline);
} else {
removeEmptyLinesBefore(comment, context.newline);
}
return;
}
const message = expectEmptyLineBefore ? messages.expected : messages.rejected;
report({
message,
node: comment,
result,
ruleName,
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,51 @@
'use strict';
const isStandardSyntaxComment = require('../../utils/isStandardSyntaxComment');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'comment-no-empty';
const messages = ruleMessages(ruleName, {
rejected: 'Unexpected empty comment',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/comment-no-empty',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual: primary });
if (!validOptions) {
return;
}
root.walkComments((comment) => {
// To ignore non-standard comments
if (!isStandardSyntaxComment(comment)) {
return;
}
// To ignore comments that are not empty
if (comment.text && comment.text.length !== 0) {
return;
}
report({
message: messages.rejected,
node: comment,
result,
ruleName,
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,52 @@
'use strict';
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'comment-pattern';
const messages = ruleMessages(ruleName, {
expected: (pattern) => `Expected comment to match pattern "${pattern}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/comment-pattern',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [isRegExp, isString],
});
if (!validOptions) {
return;
}
const normalizedPattern = isString(primary) ? new RegExp(primary) : primary;
root.walkComments((comment) => {
const text = comment.text;
if (normalizedPattern.test(text)) {
return;
}
report({
message: messages.expected(primary),
node: comment,
result,
ruleName,
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,141 @@
'use strict';
const isStandardSyntaxComment = require('../../utils/isStandardSyntaxComment');
const isWhitespace = require('../../utils/isWhitespace');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'comment-whitespace-inside';
const messages = ruleMessages(ruleName, {
expectedOpening: 'Expected whitespace after "/*"',
rejectedOpening: 'Unexpected whitespace after "/*"',
expectedClosing: 'Expected whitespace before "*/"',
rejectedClosing: 'Unexpected whitespace before "*/"',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/comment-whitespace-inside',
fixable: true,
};
/**
* @param {import('postcss').Comment} comment
*/
function addWhitespaceBefore(comment) {
if (comment.text.startsWith('*')) {
comment.text = comment.text.replace(/^(\*+)/, `$1 `);
} else {
comment.raws.left = ' ';
}
}
/**
* @param {import('postcss').Comment} comment
*/
function addWhitespaceAfter(comment) {
if (comment.text[comment.text.length - 1] === '*') {
comment.text = comment.text.replace(/(\*+)$/, ` $1`);
} else {
comment.raws.right = ' ';
}
}
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'never'],
});
if (!validOptions) {
return;
}
root.walkComments((comment) => {
if (!isStandardSyntaxComment(comment)) {
return;
}
const rawComment = comment.toString();
const firstFourChars = rawComment.slice(0, 4);
// Return early if sourcemap or copyright comment
if (/^\/\*[#!]\s/.test(firstFourChars)) {
return;
}
const leftMatches = rawComment.match(/(^\/\*+)(\s)?/);
if (leftMatches == null || leftMatches[1] == null) {
throw new Error(`Invalid comment: "${rawComment}"`);
}
const rightMatches = rawComment.match(/(\s)?(\*+\/)$/);
if (rightMatches == null || rightMatches[2] == null) {
throw new Error(`Invalid comment: "${rawComment}"`);
}
const opener = leftMatches[1];
const leftSpace = leftMatches[2] || '';
const rightSpace = rightMatches[1] || '';
const closer = rightMatches[2];
if (primary === 'never' && leftSpace !== '') {
complain(messages.rejectedOpening, opener.length);
}
if (primary === 'always' && !isWhitespace(leftSpace)) {
complain(messages.expectedOpening, opener.length);
}
if (primary === 'never' && rightSpace !== '') {
complain(messages.rejectedClosing, comment.toString().length - closer.length - 1);
}
if (primary === 'always' && !isWhitespace(rightSpace)) {
complain(messages.expectedClosing, comment.toString().length - closer.length - 1);
}
/**
* @param {string} message
* @param {number} index
*/
function complain(message, index) {
if (context.fix) {
if (primary === 'never') {
comment.raws.left = '';
comment.raws.right = '';
comment.text = comment.text.replace(/^(\*+)(\s+)?/, '$1').replace(/(\s+)?(\*+)$/, '$2');
} else {
if (!leftSpace) {
addWhitespaceBefore(comment);
}
if (!rightSpace) {
addWhitespaceAfter(comment);
}
}
return;
}
report({
message,
index,
result,
ruleName,
node: comment,
});
}
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,64 @@
'use strict';
const containsString = require('../../utils/containsString');
const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'comment-word-disallowed-list';
const messages = ruleMessages(ruleName, {
rejected: (pattern) => `Unexpected word matching pattern "${pattern}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/comment-word-disallowed-list',
};
/** @type {import('stylelint').Rule<string | RegExp | Array<string | RegExp>>} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [isString, isRegExp],
});
if (!validOptions) {
return;
}
root.walkComments((comment) => {
const text = comment.text;
const rawComment = comment.toString();
const firstFourChars = rawComment.slice(0, 4);
// Return early if sourcemap
if (firstFourChars === '/*# ') {
return;
}
const matchesWord = matchesStringOrRegExp(text, primary) || containsString(text, primary);
if (!matchesWord) {
return;
}
report({
message: messages.rejected(matchesWord.pattern),
node: comment,
word: matchesWord.substring,
result,
ruleName,
});
});
};
};
rule.primaryOptionArray = true;
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,66 @@
'use strict';
const atRuleParamIndex = require('../../utils/atRuleParamIndex');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'custom-media-pattern';
const messages = ruleMessages(ruleName, {
expected: (mediaName, pattern) => `Expected "${mediaName}" to match pattern "${pattern}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/custom-media-pattern',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [isRegExp, isString],
});
if (!validOptions) {
return;
}
const regexpPattern = isString(primary) ? new RegExp(primary) : primary;
root.walkAtRules((atRule) => {
if (atRule.name.toLowerCase() !== 'custom-media') {
return;
}
const [fullName, customMediaName] = atRule.params.match(/^--(\S+)\b/) || [];
if (fullName === undefined || customMediaName === undefined) {
throw new Error(`Unexpected at-rule params: "${atRule.params}"`);
}
if (regexpPattern.test(customMediaName)) {
return;
}
const index = atRuleParamIndex(atRule);
report({
message: messages.expected,
messageArgs: [fullName, primary],
node: atRule,
index,
endIndex: index + fullName.length,
result,
ruleName,
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,143 @@
'use strict';
const addEmptyLineBefore = require('../../utils/addEmptyLineBefore');
const blockString = require('../../utils/blockString');
const getPreviousNonSharedLineCommentNode = require('../../utils/getPreviousNonSharedLineCommentNode');
const hasEmptyLine = require('../../utils/hasEmptyLine');
const isAfterComment = require('../../utils/isAfterComment');
const isCustomProperty = require('../../utils/isCustomProperty');
const isFirstNested = require('../../utils/isFirstNested');
const isSingleLineString = require('../../utils/isSingleLineString');
const isStandardSyntaxDeclaration = require('../../utils/isStandardSyntaxDeclaration');
const optionsMatches = require('../../utils/optionsMatches');
const removeEmptyLinesBefore = require('../../utils/removeEmptyLinesBefore');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { isAtRule, isDeclaration, isRule } = require('../../utils/typeGuards');
const ruleName = 'custom-property-empty-line-before';
const messages = ruleMessages(ruleName, {
expected: 'Expected empty line before custom property',
rejected: 'Unexpected empty line before custom property',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/custom-property-empty-line-before',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: ['always', 'never'],
},
{
actual: secondaryOptions,
possible: {
except: ['first-nested', 'after-comment', 'after-custom-property'],
ignore: ['after-comment', 'first-nested', 'inside-single-line-block'],
},
optional: true,
},
);
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
const prop = decl.prop;
const parent = decl.parent;
if (!isStandardSyntaxDeclaration(decl)) {
return;
}
if (!isCustomProperty(prop)) {
return;
}
// Optionally ignore the node if a comment precedes it
if (optionsMatches(secondaryOptions, 'ignore', 'after-comment') && isAfterComment(decl)) {
return;
}
// Optionally ignore the node if it is the first nested
if (optionsMatches(secondaryOptions, 'ignore', 'first-nested') && isFirstNested(decl)) {
return;
}
// Optionally ignore nodes inside single-line blocks
if (
optionsMatches(secondaryOptions, 'ignore', 'inside-single-line-block') &&
parent != null &&
(isAtRule(parent) || isRule(parent)) &&
isSingleLineString(blockString(parent))
) {
return;
}
let expectEmptyLineBefore = primary === 'always';
// Optionally reverse the expectation if any exceptions apply
if (
(optionsMatches(secondaryOptions, 'except', 'first-nested') && isFirstNested(decl)) ||
(optionsMatches(secondaryOptions, 'except', 'after-comment') && isAfterComment(decl)) ||
(optionsMatches(secondaryOptions, 'except', 'after-custom-property') &&
isAfterCustomProperty(decl))
) {
expectEmptyLineBefore = !expectEmptyLineBefore;
}
const hasEmptyLineBefore = hasEmptyLine(decl.raws.before);
// Return if the expectation is met
if (expectEmptyLineBefore === hasEmptyLineBefore) {
return;
}
// Fix
if (context.fix) {
if (context.newline == null) return;
if (expectEmptyLineBefore) {
addEmptyLineBefore(decl, context.newline);
} else {
removeEmptyLinesBefore(decl, context.newline);
}
return;
}
const message = expectEmptyLineBefore ? messages.expected : messages.rejected;
report({
message,
node: decl,
result,
ruleName,
});
});
};
};
/**
* @param {import('postcss').Declaration} decl
*/
function isAfterCustomProperty(decl) {
const prevNode = getPreviousNonSharedLineCommentNode(decl);
return prevNode != null && isDeclaration(prevNode) && isCustomProperty(prevNode.prop);
}
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,85 @@
'use strict';
const valueParser = require('postcss-value-parser');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const isCustomProperty = require('../../utils/isCustomProperty');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'custom-property-no-missing-var-function';
const messages = ruleMessages(ruleName, {
rejected: (customProperty) => `Unexpected missing var function for "${customProperty}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/custom-property-no-missing-var-function',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual: primary });
if (!validOptions) return;
/** @type {Set<string>} */
const knownCustomProperties = new Set();
root.walkAtRules(/^property$/i, (atRule) => {
knownCustomProperties.add(atRule.params);
});
root.walkDecls(({ prop }) => {
if (isCustomProperty(prop)) knownCustomProperties.add(prop);
});
root.walkDecls((decl) => {
const { value } = decl;
const parsedValue = valueParser(value);
parsedValue.walk((node) => {
if (isVarFunction(node)) return false;
if (!isDashedIdent(node)) return;
if (!knownCustomProperties.has(node.value)) return;
const index = declarationValueIndex(decl) + node.sourceIndex;
const endIndex = index + node.value.length;
report({
message: messages.rejected(node.value),
node: decl,
index,
endIndex,
result,
ruleName,
});
return false;
});
});
};
};
/**
* @param {import('postcss-value-parser').Node} node
*/
function isDashedIdent({ type, value }) {
return type === 'word' && value.startsWith('--');
}
/**
* @param {import('postcss-value-parser').Node} node
*/
function isVarFunction({ type, value }) {
return type === 'function' && value === 'var';
}
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,95 @@
'use strict';
const valueParser = require('postcss-value-parser');
const isCustomProperty = require('../../utils/isCustomProperty');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const { isRegExp, isString } = require('../../utils/validateTypes');
const { isValueFunction } = require('../../utils/typeGuards');
const isStandardSyntaxProperty = require('../../utils/isStandardSyntaxProperty');
const ruleName = 'custom-property-pattern';
const messages = ruleMessages(ruleName, {
expected: (propName, pattern) => `Expected "${propName}" to match pattern "${pattern}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/custom-property-pattern',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [isRegExp, isString],
});
if (!validOptions) {
return;
}
const regexpPattern = isString(primary) ? new RegExp(primary) : primary;
/**
* @param {string} property
* @returns {boolean}
*/
function check(property) {
return (
!isStandardSyntaxProperty(property) ||
!isCustomProperty(property) ||
regexpPattern.test(property.slice(2))
);
}
root.walkDecls((decl) => {
const { prop, value } = decl;
const parsedValue = valueParser(value);
parsedValue.walk((node) => {
if (!isValueFunction(node)) return;
if (node.value.toLowerCase() !== 'var') return;
const { nodes } = node;
const firstNode = nodes[0];
if (!firstNode || check(firstNode.value)) return;
complain(declarationValueIndex(decl) + firstNode.sourceIndex, firstNode.value, decl);
});
if (check(prop)) return;
complain(0, prop, decl);
});
/**
* @param {number} index
* @param {string} propName
* @param {import('postcss').Declaration} decl
*/
function complain(index, propName, decl) {
report({
result,
ruleName,
message: messages.expected,
messageArgs: [propName, primary],
node: decl,
index,
endIndex: index + propName.length,
});
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,90 @@
'use strict';
const declarationBangSpaceChecker = require('../declarationBangSpaceChecker');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const ruleMessages = require('../../utils/ruleMessages');
const setDeclarationValue = require('../../utils/setDeclarationValue');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'declaration-bang-space-after';
const messages = ruleMessages(ruleName, {
expectedAfter: () => 'Expected single space after "!"',
rejectedAfter: () => 'Unexpected whitespace after "!"',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-bang-space-after',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
const checker = whitespaceChecker('space', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'never'],
});
if (!validOptions) {
return;
}
declarationBangSpaceChecker({
root,
result,
locationChecker: checker.after,
checkedRuleName: ruleName,
fix: context.fix
? (decl, index) => {
let bangIndex = index - declarationValueIndex(decl);
const declValue = getDeclarationValue(decl);
let target;
/** @type {(value: string) => void} */
let setFixed;
if (bangIndex < declValue.length) {
target = declValue;
setFixed = (value) => {
setDeclarationValue(decl, value);
};
} else if (decl.important) {
target = decl.raws.important || ' !important';
bangIndex -= declValue.length;
setFixed = (value) => {
decl.raws.important = value;
};
} else {
return false; // not standard
}
const targetBefore = target.slice(0, bangIndex + 1);
const targetAfter = target.slice(bangIndex + 1);
if (primary === 'always') {
setFixed(targetBefore + targetAfter.replace(/^\s*/, ' '));
return true;
}
if (primary === 'never') {
setFixed(targetBefore + targetAfter.replace(/^\s*/, ''));
return true;
}
return false;
}
: null,
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,91 @@
'use strict';
const declarationBangSpaceChecker = require('../declarationBangSpaceChecker');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const ruleMessages = require('../../utils/ruleMessages');
const setDeclarationValue = require('../../utils/setDeclarationValue');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'declaration-bang-space-before';
const messages = ruleMessages(ruleName, {
expectedBefore: () => 'Expected single space before "!"',
rejectedBefore: () => 'Unexpected whitespace before "!"',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-bang-space-before',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
const checker = whitespaceChecker('space', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'never'],
});
if (!validOptions) {
return;
}
declarationBangSpaceChecker({
root,
result,
locationChecker: checker.before,
checkedRuleName: ruleName,
fix: context.fix
? (decl, index) => {
let bangIndex = index - declarationValueIndex(decl);
const value = getDeclarationValue(decl);
let target;
/** @type {(val: string) => void} */
let setFixed;
if (bangIndex < value.length) {
target = value;
setFixed = (val) => {
setDeclarationValue(decl, val);
};
} else if (decl.important) {
target = decl.raws.important || ' !important';
bangIndex -= value.length;
setFixed = (val) => {
decl.raws.important = val;
};
} else {
return false; // not standard
}
const targetBefore = target.slice(0, bangIndex);
const targetAfter = target.slice(bangIndex);
if (primary === 'always') {
// eslint-disable-next-line prefer-template
setFixed(targetBefore.replace(/\s*$/, '') + ' ' + targetAfter);
return true;
}
if (primary === 'never') {
setFixed(targetBefore.replace(/\s*$/, '') + targetAfter);
return true;
}
return false;
}
: null,
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,66 @@
'use strict';
const eachDeclarationBlock = require('../../utils/eachDeclarationBlock');
const isCustomProperty = require('../../utils/isCustomProperty');
const isStandardSyntaxProperty = require('../../utils/isStandardSyntaxProperty');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'declaration-block-no-duplicate-custom-properties';
const messages = ruleMessages(ruleName, {
rejected: (property) => `Unexpected duplicate "${property}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-block-no-duplicate-custom-properties',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual: primary });
if (!validOptions) {
return;
}
eachDeclarationBlock(root, (eachDecl) => {
const decls = new Set();
eachDecl((decl) => {
const prop = decl.prop;
if (!isStandardSyntaxProperty(prop)) {
return;
}
if (!isCustomProperty(prop)) {
return;
}
const isDuplicate = decls.has(prop);
if (isDuplicate) {
report({
message: messages.rejected(prop),
node: decl,
result,
ruleName,
word: prop,
});
return;
}
decls.add(prop);
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,164 @@
'use strict';
const eachDeclarationBlock = require('../../utils/eachDeclarationBlock');
const isCustomProperty = require('../../utils/isCustomProperty');
const isStandardSyntaxProperty = require('../../utils/isStandardSyntaxProperty');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { isString } = require('../../utils/validateTypes');
const vendor = require('../../utils/vendor');
const ruleName = 'declaration-block-no-duplicate-properties';
const messages = ruleMessages(ruleName, {
rejected: (property) => `Unexpected duplicate "${property}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-block-no-duplicate-properties',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{ actual: primary },
{
actual: secondaryOptions,
possible: {
ignore: [
'consecutive-duplicates',
'consecutive-duplicates-with-different-values',
'consecutive-duplicates-with-same-prefixless-values',
],
ignoreProperties: [isString],
},
optional: true,
},
);
if (!validOptions) {
return;
}
const ignoreDuplicates = optionsMatches(secondaryOptions, 'ignore', 'consecutive-duplicates');
const ignoreDiffValues = optionsMatches(
secondaryOptions,
'ignore',
'consecutive-duplicates-with-different-values',
);
const ignorePrefixlessSameValues = optionsMatches(
secondaryOptions,
'ignore',
'consecutive-duplicates-with-same-prefixless-values',
);
eachDeclarationBlock(root, (eachDecl) => {
/** @type {import('postcss').Declaration[]} */
const decls = [];
eachDecl((decl) => {
const prop = decl.prop;
const lowerProp = decl.prop.toLowerCase();
const value = decl.value;
const important = decl.important;
if (!isStandardSyntaxProperty(prop)) {
return;
}
if (isCustomProperty(prop)) {
return;
}
// Return early if the property is to be ignored
if (optionsMatches(secondaryOptions, 'ignoreProperties', prop)) {
return;
}
// Ignore the src property as commonly duplicated in at-fontface
if (lowerProp === 'src') {
return;
}
const indexDuplicate = decls.findIndex((d) => d.prop.toLowerCase() === lowerProp);
if (indexDuplicate === -1) {
decls.push(decl);
}
const duplicateDecl = decls[indexDuplicate];
if (!duplicateDecl) {
return;
}
const duplicateValue = duplicateDecl.value || '';
const duplicateImportant = duplicateDecl.important || false;
const duplicateIsMoreImportant = !important && duplicateImportant;
const duplicatesAreConsecutive = indexDuplicate === decls.length - 1;
const unprefixedDuplicatesAreEqual =
vendor.unprefixed(value) === vendor.unprefixed(duplicateValue);
const fixOrReport = () => {
if (!context.fix) {
return report({
message: messages.rejected(prop),
node: decl,
result,
ruleName,
word: prop,
});
}
if (duplicateIsMoreImportant) {
return decl.remove();
}
return duplicateDecl.remove();
};
if (ignoreDiffValues || ignorePrefixlessSameValues) {
if (
!duplicatesAreConsecutive ||
(ignorePrefixlessSameValues && !unprefixedDuplicatesAreEqual)
) {
fixOrReport();
}
if (value !== duplicateValue) {
return;
}
if (context.fix) {
return duplicateDecl.remove();
}
return report({
message: messages.rejected(prop),
node: decl,
result,
ruleName,
word: prop,
});
}
if (ignoreDuplicates && duplicatesAreConsecutive) {
return;
}
fixOrReport();
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,114 @@
'use strict';
const arrayEqual = require('../../utils/arrayEqual');
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',
};
const IGNORED_VALUES = new Set(['inherit']);
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{ actual: primary },
{
actual: secondaryOptions,
possible: {
ignoreShorthands: [isString, isRegExp],
},
optional: true,
},
);
if (!validOptions) {
return;
}
/** @type {Map<string, string[]>} */
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();
eachDecl((decl) => {
if (IGNORED_VALUES.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) || [];
longhandDeclaration.push(prop);
longhandDeclarations.set(prefixedShorthandProperty, longhandDeclaration);
const shorthandProps = /** @type {Map<string, Set<string>>} */ (
longhandSubPropertiesOfShorthandProperties
).get(shorthandProperty);
const prefixedShorthandData = Array.from(shorthandProps || []).map(
(item) => prefix + item,
);
if (!arrayEqual(prefixedShorthandData.sort(), longhandDeclaration.sort())) {
continue;
}
report({
ruleName,
result,
node: decl,
word: decl.prop,
message: messages.expected(prefixedShorthandProperty),
});
}
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,71 @@
'use strict';
const eachDeclarationBlock = require('../../utils/eachDeclarationBlock');
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 ruleName = 'declaration-block-no-shorthand-property-overrides';
const messages = ruleMessages(ruleName, {
rejected: (shorthand, original) => `Unexpected shorthand "${shorthand}" after "${original}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-block-no-shorthand-property-overrides',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual: primary });
if (!validOptions) {
return;
}
eachDeclarationBlock(root, (eachDecl) => {
/** @type {Map<string, string>} */
const declarations = new Map();
eachDecl((decl) => {
const prop = decl.prop;
const unprefixedProp = vendor.unprefixed(prop).toLowerCase();
const prefix = vendor.prefix(prop).toLowerCase();
const overrideables = /** @type {Map<string, Set<string>>} */ (
longhandSubPropertiesOfShorthandProperties
).get(unprefixedProp);
if (!overrideables) {
declarations.set(prop.toLowerCase(), prop);
return;
}
for (const longhandProp of overrideables) {
const declaration = declarations.get(prefix + longhandProp);
if (!declaration) {
continue;
}
report({
ruleName,
result,
node: decl,
message: messages.rejected(prop, declaration || ''),
word: prop,
});
}
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,106 @@
'use strict';
const blockString = require('../../utils/blockString');
const nextNonCommentNode = require('../../utils/nextNonCommentNode');
const rawNodeString = require('../../utils/rawNodeString');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const { isAtRule, isRule } = require('../../utils/typeGuards');
const ruleName = 'declaration-block-semicolon-newline-after';
const messages = ruleMessages(ruleName, {
expectedAfter: () => 'Expected newline after ";"',
expectedAfterMultiLine: () => 'Expected newline after ";" in a multi-line declaration block',
rejectedAfterMultiLine: () => 'Unexpected newline after ";" in a multi-line declaration block',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-block-semicolon-newline-after',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
const checker = whitespaceChecker('newline', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'always-multi-line', 'never-multi-line'],
});
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
// Ignore last declaration if there's no trailing semicolon
const parentRule = decl.parent;
if (!parentRule) throw new Error('A parent node must be present');
if (!isAtRule(parentRule) && !isRule(parentRule)) {
return;
}
if (!parentRule.raws.semicolon && parentRule.last === decl) {
return;
}
const nextNode = decl.next();
if (!nextNode) {
return;
}
// Allow end-of-line comment
const nodeToCheck = nextNonCommentNode(nextNode);
if (!nodeToCheck) {
return;
}
checker.afterOneOnly({
source: rawNodeString(nodeToCheck),
index: -1,
lineCheckStr: blockString(parentRule),
err: (m) => {
if (context.fix) {
if (primary.startsWith('always')) {
const index = nodeToCheck.raws.before.search(/\r?\n/);
nodeToCheck.raws.before =
index >= 0
? nodeToCheck.raws.before.slice(index)
: context.newline + nodeToCheck.raws.before;
return;
}
if (primary === 'never-multi-line') {
nodeToCheck.raws.before = '';
return;
}
}
report({
message: m,
node: decl,
index: decl.toString().length + 1,
result,
ruleName,
});
},
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,73 @@
'use strict';
const blockString = require('../../utils/blockString');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const { isAtRule, isRule } = require('../../utils/typeGuards');
const ruleName = 'declaration-block-semicolon-newline-before';
const messages = ruleMessages(ruleName, {
expectedBefore: () => 'Expected newline before ";"',
expectedBeforeMultiLine: () => 'Expected newline before ";" in a multi-line declaration block',
rejectedBeforeMultiLine: () =>
'Unexpected whitespace before ";" in a multi-line declaration block',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-block-semicolon-newline-before',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
const checker = whitespaceChecker('newline', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'always-multi-line', 'never-multi-line'],
});
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
const parentRule = decl.parent;
if (!parentRule) throw new Error('A parent node must be present');
if (!isAtRule(parentRule) && !isRule(parentRule)) {
return;
}
if (!parentRule.raws.semicolon && parentRule.last === decl) {
return;
}
const declString = decl.toString();
checker.beforeAllowingIndentation({
source: declString,
index: declString.length,
lineCheckStr: blockString(parentRule),
err: (m) => {
report({
message: m,
node: decl,
index: decl.toString().length - 1,
result,
ruleName,
});
},
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,96 @@
'use strict';
const blockString = require('../../utils/blockString');
const rawNodeString = require('../../utils/rawNodeString');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const { isAtRule, isRule } = require('../../utils/typeGuards');
const ruleName = 'declaration-block-semicolon-space-after';
const messages = ruleMessages(ruleName, {
expectedAfter: () => 'Expected single space after ";"',
rejectedAfter: () => 'Unexpected whitespace after ";"',
expectedAfterSingleLine: () =>
'Expected single space after ";" in a single-line declaration block',
rejectedAfterSingleLine: () =>
'Unexpected whitespace after ";" in a single-line declaration block',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-block-semicolon-space-after',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
const checker = whitespaceChecker('space', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'never', 'always-single-line', 'never-single-line'],
});
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
// Ignore last declaration if there's no trailing semicolon
const parentRule = decl.parent;
if (!parentRule) throw new Error('A parent node must be present');
if (!isAtRule(parentRule) && !isRule(parentRule)) {
return;
}
if (!parentRule.raws.semicolon && parentRule.last === decl) {
return;
}
const nextDecl = decl.next();
if (!nextDecl) {
return;
}
checker.after({
source: rawNodeString(nextDecl),
index: -1,
lineCheckStr: blockString(parentRule),
err: (m) => {
if (context.fix) {
if (primary.startsWith('always')) {
nextDecl.raws.before = ' ';
return;
}
if (primary.startsWith('never')) {
nextDecl.raws.before = '';
return;
}
}
report({
message: m,
node: decl,
index: decl.toString().length + 1,
result,
ruleName,
});
},
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,103 @@
'use strict';
const blockString = require('../../utils/blockString');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const setDeclarationValue = require('../../utils/setDeclarationValue');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const { isAtRule, isRule } = require('../../utils/typeGuards');
const ruleName = 'declaration-block-semicolon-space-before';
const messages = ruleMessages(ruleName, {
expectedBefore: () => 'Expected single space before ";"',
rejectedBefore: () => 'Unexpected whitespace before ";"',
expectedBeforeSingleLine: () =>
'Expected single space before ";" in a single-line declaration block',
rejectedBeforeSingleLine: () =>
'Unexpected whitespace before ";" in a single-line declaration block',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-block-semicolon-space-before',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
const checker = whitespaceChecker('space', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'never', 'always-single-line', 'never-single-line'],
});
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
// Ignore last declaration if there's no trailing semicolon
const parentRule = decl.parent;
if (!parentRule) throw new Error('A parent node must be present');
if (!isAtRule(parentRule) && !isRule(parentRule)) {
return;
}
if (!parentRule.raws.semicolon && parentRule.last === decl) {
return;
}
const declString = decl.toString();
checker.before({
source: declString,
index: declString.length,
lineCheckStr: blockString(parentRule),
err: (m) => {
if (context.fix) {
const value = getDeclarationValue(decl);
if (primary.startsWith('always')) {
if (decl.important) {
decl.raws.important = ' !important ';
} else {
setDeclarationValue(decl, value.replace(/\s*$/, ' '));
}
return;
}
if (primary.startsWith('never')) {
if (decl.raws.important) {
decl.raws.important = decl.raws.important.replace(/\s*$/, '');
} else {
setDeclarationValue(decl, value.replace(/\s*$/, ''));
}
return;
}
}
report({
message: m,
node: decl,
index: decl.toString().length - 1,
result,
ruleName,
});
},
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,63 @@
'use strict';
const blockString = require('../../utils/blockString');
const isSingleLineString = require('../../utils/isSingleLineString');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { isNumber } = require('../../utils/validateTypes');
const ruleName = 'declaration-block-single-line-max-declarations';
const messages = ruleMessages(ruleName, {
expected: (max) => `Expected no more than ${max} ${max === 1 ? 'declaration' : 'declarations'}`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-block-single-line-max-declarations',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [isNumber],
});
if (!validOptions) {
return;
}
root.walkRules((ruleNode) => {
const block = blockString(ruleNode);
if (!isSingleLineString(block)) {
return;
}
if (!ruleNode.nodes) {
return;
}
const decls = ruleNode.nodes.filter((node) => node.type === 'decl');
if (decls.length <= primary) {
return;
}
report({
message: messages.expected(primary),
node: ruleNode,
word: block,
result,
ruleName,
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,145 @@
'use strict';
const hasBlock = require('../../utils/hasBlock');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const { isAtRule } = require('../../utils/typeGuards');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'declaration-block-trailing-semicolon';
const messages = ruleMessages(ruleName, {
expected: 'Expected a trailing semicolon',
rejected: 'Unexpected trailing semicolon',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-block-trailing-semicolon',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: ['always', 'never'],
},
{
actual: secondaryOptions,
possible: {
ignore: ['single-declaration'],
},
optional: true,
},
);
if (!validOptions) {
return;
}
root.walkAtRules((atRule) => {
if (!atRule.parent) throw new Error('A parent node must be present');
if (atRule.parent === root) {
return;
}
if (atRule !== atRule.parent.last) {
return;
}
if (hasBlock(atRule)) {
return;
}
checkLastNode(atRule);
});
root.walkDecls((decl) => {
if (!decl.parent) throw new Error('A parent node must be present');
if (decl.parent.type === 'object') {
return;
}
if (decl !== decl.parent.last) {
return;
}
checkLastNode(decl);
});
/**
* @param {import('postcss').Node} node
*/
function checkLastNode(node) {
if (!node.parent) throw new Error('A parent node must be present');
const hasSemicolon = node.parent.raws.semicolon;
const ignoreSingleDeclaration = optionsMatches(
secondaryOptions,
'ignore',
'single-declaration',
);
if (ignoreSingleDeclaration && node.parent.first === node) {
return;
}
let message;
if (primary === 'always') {
if (hasSemicolon) {
return;
}
// auto-fix
if (context.fix) {
node.parent.raws.semicolon = true;
if (isAtRule(node)) {
node.raws.between = '';
node.parent.raws.after = ' ';
}
return;
}
message = messages.expected;
} else if (primary === 'never') {
if (!hasSemicolon) {
return;
}
// auto-fix
if (context.fix) {
node.parent.raws.semicolon = false;
return;
}
message = messages.rejected;
} else {
throw new Error(`Unexpected primary option: "${primary}"`);
}
report({
message,
node,
index: node.toString().trim().length - 1,
result,
ruleName,
});
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,96 @@
'use strict';
const declarationValueIndex = require('../../utils/declarationValueIndex');
const isStandardSyntaxDeclaration = require('../../utils/isStandardSyntaxDeclaration');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'declaration-colon-newline-after';
const messages = ruleMessages(ruleName, {
expectedAfter: () => 'Expected newline after ":"',
expectedAfterMultiLine: () => 'Expected newline after ":" with a multi-line declaration',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-colon-newline-after',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
const checker = whitespaceChecker('newline', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'always-multi-line'],
});
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
if (!isStandardSyntaxDeclaration(decl)) {
return;
}
// Get the raw prop, and only the prop
const endOfPropIndex = declarationValueIndex(decl) + (decl.raws.between || '').length - 1;
// The extra characters tacked onto the end ensure that there is a character to check
// after the colon. Otherwise, with `background:pink` the character after the
const propPlusColon = `${decl.toString().slice(0, endOfPropIndex)}xxx`;
for (let i = 0, l = propPlusColon.length; i < l; i++) {
if (propPlusColon[i] !== ':') {
continue;
}
const indexToCheck = /^[^\S\r\n]*\/\*/.test(propPlusColon.slice(i + 1))
? propPlusColon.indexOf('*/', i) + 1
: i;
checker.afterOneOnly({
source: propPlusColon,
index: indexToCheck,
lineCheckStr: decl.value,
err: (m) => {
if (context.fix) {
const between = decl.raws.between;
if (between == null) throw new Error('`between` must be present');
const betweenStart = declarationValueIndex(decl) - between.length;
const sliceIndex = indexToCheck - betweenStart + 1;
const betweenBefore = between.slice(0, sliceIndex);
const betweenAfter = between.slice(sliceIndex);
decl.raws.between = /^\s*\n/.test(betweenAfter)
? betweenBefore + betweenAfter.replace(/^[^\S\r\n]*/, '')
: betweenBefore + context.newline + betweenAfter;
return;
}
report({
message: m,
node: decl,
index: indexToCheck,
result,
ruleName,
});
},
});
}
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,72 @@
'use strict';
const declarationColonSpaceChecker = require('../declarationColonSpaceChecker');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'declaration-colon-space-after';
const messages = ruleMessages(ruleName, {
expectedAfter: () => 'Expected single space after ":"',
rejectedAfter: () => 'Unexpected whitespace after ":"',
expectedAfterSingleLine: () => 'Expected single space after ":" with a single-line declaration',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-colon-space-after',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
const checker = whitespaceChecker('space', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'never', 'always-single-line'],
});
if (!validOptions) {
return;
}
declarationColonSpaceChecker({
root,
result,
locationChecker: checker.after,
checkedRuleName: ruleName,
fix: context.fix
? (decl, index) => {
const colonIndex = index - declarationValueIndex(decl);
const between = decl.raws.between;
if (between == null) throw new Error('`between` must be present');
if (primary.startsWith('always')) {
decl.raws.between =
between.slice(0, colonIndex) + between.slice(colonIndex).replace(/^:\s*/, ': ');
return true;
}
if (primary === 'never') {
decl.raws.between =
between.slice(0, colonIndex) + between.slice(colonIndex).replace(/^:\s*/, ':');
return true;
}
return false;
}
: null,
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,71 @@
'use strict';
const declarationColonSpaceChecker = require('../declarationColonSpaceChecker');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'declaration-colon-space-before';
const messages = ruleMessages(ruleName, {
expectedBefore: () => 'Expected single space before ":"',
rejectedBefore: () => 'Unexpected whitespace before ":"',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-colon-space-before',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
const checker = whitespaceChecker('space', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'never'],
});
if (!validOptions) {
return;
}
declarationColonSpaceChecker({
root,
result,
locationChecker: checker.before,
checkedRuleName: ruleName,
fix: context.fix
? (decl, index) => {
const colonIndex = index - declarationValueIndex(decl);
const between = decl.raws.between;
if (between == null) throw new Error('`between` must be present');
if (primary === 'always') {
decl.raws.between =
between.slice(0, colonIndex).replace(/\s*$/, ' ') + between.slice(colonIndex);
return true;
}
if (primary === 'never') {
decl.raws.between =
between.slice(0, colonIndex).replace(/\s*$/, '') + between.slice(colonIndex);
return true;
}
return false;
}
: null,
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,155 @@
'use strict';
const addEmptyLineBefore = require('../../utils/addEmptyLineBefore');
const blockString = require('../../utils/blockString');
const hasEmptyLine = require('../../utils/hasEmptyLine');
const isAfterComment = require('../../utils/isAfterComment');
const isAfterStandardPropertyDeclaration = require('../../utils/isAfterStandardPropertyDeclaration');
const isCustomProperty = require('../../utils/isCustomProperty');
const isFirstNested = require('../../utils/isFirstNested');
const isFirstNodeOfRoot = require('../../utils/isFirstNodeOfRoot');
const isSingleLineString = require('../../utils/isSingleLineString');
const isStandardSyntaxDeclaration = require('../../utils/isStandardSyntaxDeclaration');
const optionsMatches = require('../../utils/optionsMatches');
const removeEmptyLinesBefore = require('../../utils/removeEmptyLinesBefore');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { isAtRule, isRule, isRoot } = require('../../utils/typeGuards');
const ruleName = 'declaration-empty-line-before';
const messages = ruleMessages(ruleName, {
expected: 'Expected empty line before declaration',
rejected: 'Unexpected empty line before declaration',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-empty-line-before',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: ['always', 'never'],
},
{
actual: secondaryOptions,
possible: {
except: ['first-nested', 'after-comment', 'after-declaration'],
ignore: [
'after-comment',
'after-declaration',
'first-nested',
'inside-single-line-block',
],
},
optional: true,
},
);
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
const prop = decl.prop;
const parent = decl.parent;
if (parent == null) {
return;
}
// Ignore the first node
if (isFirstNodeOfRoot(decl)) {
return;
}
if (!isAtRule(parent) && !isRule(parent) && !isRoot(parent)) {
return;
}
if (!isStandardSyntaxDeclaration(decl)) {
return;
}
if (isCustomProperty(prop)) {
return;
}
// Optionally ignore the node if a comment precedes it
if (optionsMatches(secondaryOptions, 'ignore', 'after-comment') && isAfterComment(decl)) {
return;
}
// Optionally ignore the node if a declaration precedes it
if (
optionsMatches(secondaryOptions, 'ignore', 'after-declaration') &&
isAfterStandardPropertyDeclaration(decl)
) {
return;
}
// Optionally ignore the node if it is the first nested
if (optionsMatches(secondaryOptions, 'ignore', 'first-nested') && isFirstNested(decl)) {
return;
}
// Optionally ignore nodes inside single-line blocks
if (
optionsMatches(secondaryOptions, 'ignore', 'inside-single-line-block') &&
isSingleLineString(blockString(parent))
) {
return;
}
let expectEmptyLineBefore = primary === 'always';
// Optionally reverse the expectation if any exceptions apply
if (
(optionsMatches(secondaryOptions, 'except', 'first-nested') && isFirstNested(decl)) ||
(optionsMatches(secondaryOptions, 'except', 'after-comment') && isAfterComment(decl)) ||
(optionsMatches(secondaryOptions, 'except', 'after-declaration') &&
isAfterStandardPropertyDeclaration(decl))
) {
expectEmptyLineBefore = !expectEmptyLineBefore;
}
// Check for at least one empty line
const hasEmptyLineBefore = hasEmptyLine(decl.raws.before);
// Return if the expectation is met
if (expectEmptyLineBefore === hasEmptyLineBefore) {
return;
}
// Fix
if (context.fix) {
if (context.newline == null) return;
if (expectEmptyLineBefore) {
addEmptyLineBefore(decl, context.newline);
} else {
removeEmptyLinesBefore(decl, context.newline);
}
return;
}
const message = expectEmptyLineBefore ? messages.expected : messages.rejected;
report({ message, node: decl, result, ruleName });
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,52 @@
'use strict';
const getImportantPosition = require('../../utils/getImportantPosition');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { assert } = require('../../utils/validateTypes');
const ruleName = 'declaration-no-important';
const messages = ruleMessages(ruleName, {
rejected: 'Unexpected !important',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-no-important',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual: primary });
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
if (!decl.important) {
return;
}
const pos = getImportantPosition(decl.toString());
assert(pos);
report({
message: messages.rejected,
node: decl,
index: pos.index,
endIndex: pos.endIndex,
result,
ruleName,
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,77 @@
'use strict';
const valueParser = require('postcss-value-parser');
const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const vendor = require('../../utils/vendor');
const validateOptions = require('../../utils/validateOptions');
const { isNumber, assertNumber } = require('../../utils/validateTypes');
const validateObjectWithProps = require('../../utils/validateObjectWithProps');
const ruleName = 'declaration-property-max-values';
const messages = ruleMessages(ruleName, {
rejected: (property, max) =>
`Expected "${property}" to have no more than ${max} ${max === 1 ? 'value' : 'values'}`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-property-max-values',
};
/**
* @param {valueParser.Node} node
*/
const isValueNode = (node) => {
return node.type === 'word' || node.type === 'function' || node.type === 'string';
};
/** @type {import('stylelint').Rule<Record<string, number>>} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [validateObjectWithProps(isNumber)],
});
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
const { prop, value } = decl;
const propLength = valueParser(value).nodes.filter(isValueNode).length;
const unprefixedProp = vendor.unprefixed(prop);
const propKey = Object.keys(primary).find((propIdentifier) =>
matchesStringOrRegExp(unprefixedProp, propIdentifier),
);
if (!propKey) {
return;
}
const max = primary[propKey];
assertNumber(max);
if (propLength <= max) {
return;
}
report({
message: messages.rejected(prop, max),
node: decl,
result,
ruleName,
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,111 @@
'use strict';
const valueParser = require('postcss-value-parser');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const flattenArray = require('../../utils/flattenArray');
const getDimension = require('../../utils/getDimension');
const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateObjectWithArrayProps = require('../../utils/validateObjectWithArrayProps');
const validateOptions = require('../../utils/validateOptions');
const { isString } = require('../../utils/validateTypes');
const vendor = require('../../utils/vendor');
const ruleName = 'declaration-property-unit-allowed-list';
const messages = ruleMessages(ruleName, {
rejected: (property, unit) => `Unexpected unit "${unit}" for property "${property}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-property-unit-allowed-list',
};
/** @type {import('stylelint').Rule<Record<string, string | string[]>>} */
const rule = (primary, secondaryOptions) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: [validateObjectWithArrayProps(isString)],
},
{
actual: secondaryOptions,
possible: {
ignore: ['inside-function'],
},
optional: true,
},
);
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
const prop = decl.prop;
const value = decl.value;
const unprefixedProp = vendor.unprefixed(prop);
const propKey = Object.keys(primary).find((propIdentifier) =>
matchesStringOrRegExp(unprefixedProp, propIdentifier),
);
if (!propKey) {
return;
}
const propList = flattenArray(primary[propKey]);
if (!propList) {
return;
}
valueParser(value).walk((node) => {
// Ignore wrong units within `url` function
if (node.type === 'function') {
if (node.value.toLowerCase() === 'url') {
return false;
}
if (optionsMatches(secondaryOptions, 'ignore', 'inside-function')) {
return false;
}
}
if (node.type === 'string') {
return;
}
const { unit } = getDimension(node);
if (!unit || (unit && propList.includes(unit.toLowerCase()))) {
return;
}
const index = declarationValueIndex(decl) + node.sourceIndex;
const endIndex = index + node.value.length;
report({
message: messages.rejected(prop, unit),
node: decl,
index,
endIndex,
result,
ruleName,
});
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,94 @@
'use strict';
const valueParser = require('postcss-value-parser');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const flattenArray = require('../../utils/flattenArray');
const getDimension = require('../../utils/getDimension');
const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateObjectWithArrayProps = require('../../utils/validateObjectWithArrayProps');
const validateOptions = require('../../utils/validateOptions');
const { isString } = require('../../utils/validateTypes');
const vendor = require('../../utils/vendor');
const ruleName = 'declaration-property-unit-disallowed-list';
const messages = ruleMessages(ruleName, {
rejected: (property, unit) => `Unexpected unit "${unit}" for property "${property}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-property-unit-disallowed-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;
}
root.walkDecls((decl) => {
const prop = decl.prop;
const value = decl.value;
const unprefixedProp = vendor.unprefixed(prop);
const propKey = Object.keys(primary).find((propIdentifier) =>
matchesStringOrRegExp(unprefixedProp, propIdentifier),
);
if (!propKey) {
return;
}
const propList = flattenArray(primary[propKey]);
if (!propList) {
return;
}
valueParser(value).walk((node) => {
// Ignore wrong units within `url` function
if (node.type === 'function' && node.value.toLowerCase() === 'url') {
return false;
}
if (node.type === 'string') {
return;
}
const { unit } = getDimension(node);
if (!unit || (unit && !propList.includes(unit.toLowerCase()))) {
return;
}
const index = declarationValueIndex(decl) + node.sourceIndex;
const endIndex = index + node.value.length;
report({
message: messages.rejected,
messageArgs: [prop, unit],
node: decl,
index,
endIndex,
result,
ruleName,
});
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,69 @@
'use strict';
const declarationValueIndex = require('../../utils/declarationValueIndex');
const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp');
const optionsMatches = require('../../utils/optionsMatches');
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 ruleName = 'declaration-property-value-allowed-list';
const messages = ruleMessages(ruleName, {
rejected: (property, value) => `Unexpected value "${value}" for property "${property}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-property-value-allowed-list',
};
/** @type {import('stylelint').Rule<Record<string, string | RegExp | Array<string | RegExp>>>} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [validateObjectWithArrayProps(isString, isRegExp)],
});
if (!validOptions) {
return;
}
const propKeys = Object.keys(primary);
root.walkDecls((decl) => {
const { prop, value } = decl;
const unprefixedProp = vendor.unprefixed(prop);
const propPatterns = propKeys.filter((key) => matchesStringOrRegExp(unprefixedProp, key));
if (propPatterns.length === 0) {
return;
}
if (propPatterns.some((pattern) => optionsMatches(primary, pattern, value))) {
return;
}
const index = declarationValueIndex(decl);
const endIndex = index + decl.value.length;
report({
message: messages.rejected(prop, value),
node: decl,
index,
endIndex,
result,
ruleName,
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,70 @@
'use strict';
const declarationValueIndex = require('../../utils/declarationValueIndex');
const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp');
const optionsMatches = require('../../utils/optionsMatches');
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 ruleName = 'declaration-property-value-disallowed-list';
const messages = ruleMessages(ruleName, {
rejected: (property, value) => `Unexpected value "${value}" for property "${property}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/declaration-property-value-disallowed-list',
};
/** @type {import('stylelint').Rule<Record<string, string | RegExp | Array<string | RegExp>>>} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [validateObjectWithArrayProps(isString, isRegExp)],
});
if (!validOptions) {
return;
}
const propKeys = Object.keys(primary);
root.walkDecls((decl) => {
const { prop, value } = decl;
const unprefixedProp = vendor.unprefixed(prop);
const propPatterns = propKeys.filter((key) => matchesStringOrRegExp(unprefixedProp, key));
if (propPatterns.length === 0) {
return;
}
if (propPatterns.every((pattern) => !optionsMatches(primary, pattern, value))) {
return;
}
const index = declarationValueIndex(decl);
const endIndex = index + decl.value.length;
report({
message: messages.rejected,
messageArgs: [prop, value],
node: decl,
index,
endIndex,
result,
ruleName,
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,60 @@
'use strict';
const declarationValueIndex = require('../utils/declarationValueIndex');
const report = require('../utils/report');
const styleSearch = require('style-search');
/** @typedef {import('postcss').Declaration} Declaration */
/** @typedef {(args: { source: string, index: number, err: (message: string) => void }) => void} LocationChecker */
/**
* @param {{
* root: import('postcss').Root,
* locationChecker: LocationChecker,
* result: import('stylelint').PostcssResult,
* checkedRuleName: string,
* fix: ((decl: Declaration, index: number) => boolean) | null,
* }} opts
* @returns {void}
*/
module.exports = function declarationBangSpaceChecker(opts) {
opts.root.walkDecls((decl) => {
const indexOffset = declarationValueIndex(decl);
const declString = decl.toString();
const valueString = decl.toString().slice(indexOffset);
if (!valueString.includes('!')) {
return;
}
styleSearch({ source: valueString, target: '!' }, (match) => {
check(declString, match.startIndex + indexOffset, decl);
});
});
/**
* @param {string} source
* @param {number} index
* @param {Declaration} decl
*/
function check(source, index, decl) {
opts.locationChecker({
source,
index,
err: (message) => {
if (opts.fix && opts.fix(decl, index)) {
return;
}
report({
message,
node: decl,
index,
result: opts.result,
ruleName: opts.checkedRuleName,
});
},
});
}
};

View File

@@ -0,0 +1,57 @@
'use strict';
const declarationValueIndex = require('../utils/declarationValueIndex');
const isStandardSyntaxDeclaration = require('../utils/isStandardSyntaxDeclaration');
const report = require('../utils/report');
/** @typedef {(args: { source: string, index: number, lineCheckStr: string, err: (message: string) => void }) => void} LocationChecker */
/**
* @param {{
* root: import('postcss').Root,
* locationChecker: LocationChecker,
* fix: ((decl: import('postcss').Declaration, index: number) => boolean) | null,
* result: import('stylelint').PostcssResult,
* checkedRuleName: string,
* }} opts
*/
module.exports = function declarationColonSpaceChecker(opts) {
opts.root.walkDecls((decl) => {
if (!isStandardSyntaxDeclaration(decl)) {
return;
}
// Get the raw prop, and only the prop
const endOfPropIndex = declarationValueIndex(decl) + (decl.raws.between || '').length - 1;
// The extra characters tacked onto the end ensure that there is a character to check
// after the colon. Otherwise, with `background:pink` the character after the
const propPlusColon = `${decl.toString().slice(0, endOfPropIndex)}xxx`;
for (let i = 0, l = propPlusColon.length; i < l; i++) {
if (propPlusColon[i] !== ':') {
continue;
}
opts.locationChecker({
source: propPlusColon,
index: i,
lineCheckStr: decl.value,
err: (message) => {
if (opts.fix && opts.fix(decl, i)) {
return;
}
report({
message,
node: decl,
index: decl.prop.toString().length + 1,
result: opts.result,
ruleName: opts.checkedRuleName,
});
},
});
break;
}
});
};

30
node_modules/stylelint/lib/rules/findMediaOperator.js generated vendored Normal file
View File

@@ -0,0 +1,30 @@
'use strict';
const styleSearch = require('style-search');
const rangeOperators = ['>=', '<=', '>', '<', '='];
/** @typedef {import('style-search').StyleSearchMatch} StyleSearchMatch */
/**
* @template {import('postcss').AtRule} T
* @param {T} atRule
* @param {(match: StyleSearchMatch, params: string, atRule: T) => void} cb
*/
module.exports = function findMediaOperator(atRule, cb) {
if (atRule.name.toLowerCase() !== 'media') {
return;
}
const params = atRule.raws.params ? atRule.raws.params.raw : atRule.params;
styleSearch({ source: params, target: rangeOperators }, (match) => {
const before = params[match.startIndex - 1];
if (before === '>' || before === '<') {
return;
}
cb(match, params, atRule);
});
};

View File

@@ -0,0 +1,269 @@
'use strict';
const findFontFamily = require('../../utils/findFontFamily');
const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
const isVariable = require('../../utils/isVariable');
const { fontFamilyKeywords } = require('../../reference/keywords');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'font-family-name-quotes';
const messages = ruleMessages(ruleName, {
expected: (family) => `Expected quotes around "${family}"`,
rejected: (family) => `Unexpected quotes around "${family}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/font-family-name-quotes',
fixable: true,
};
/**
* @param {string} font
* @returns {boolean}
*/
function isSystemFontKeyword(font) {
if (font.startsWith('-apple-')) {
return true;
}
if (font === 'BlinkMacSystemFont') {
return true;
}
return false;
}
/**
* "To avoid mistakes in escaping, it is recommended to quote font family names
* that contain white space, digits, or punctuation characters other than hyphens"
* (https://www.w3.org/TR/CSS2/fonts.html#font-family-prop)
*
* @param {string} family
* @returns {boolean}
*/
function quotesRecommended(family) {
return !/^[-a-zA-Z]+$/.test(family);
}
/**
* Quotes are required if the family is not a valid CSS identifier
* (regexes from https://mathiasbynens.be/notes/unquoted-font-family)
*
* @param {string} family
* @returns {boolean}
*/
function quotesRequired(family) {
return family
.split(/\s+/)
.some((word) => /^(?:-?\d|--)/.test(word) || !/^[-\w\u{00A0}-\u{10FFFF}]+$/u.test(word));
}
/**
* @typedef {{
* name: string,
* rawName: string,
* hasQuotes: boolean,
* sourceIndex: number,
* resetIndexes: (offset: number) => void,
* removeQuotes: () => void,
* addQuotes: () => void,
* }} MutableNode
*/
/**
*
* @param {import('postcss-value-parser').Node[]} fontFamilies
* @param {import('postcss').Declaration} decl
* @returns {MutableNode[]}
*/
const makeMutableFontFamilies = (fontFamilies, decl) => {
/**
* @type {MutableNode[]}
*/
const mutableNodes = [];
fontFamilies.forEach((fontFamily, idx) => {
const quote = 'quote' in fontFamily && fontFamily.quote;
const name = fontFamily.value;
/** @type {MutableNode} */
const newNode = {
name,
rawName: quote ? `${quote}${name}${quote}` : name,
sourceIndex: fontFamily.sourceIndex,
hasQuotes: Boolean(quote),
resetIndexes(offset) {
mutableNodes.slice(idx + 1).forEach((n) => (n.sourceIndex += offset));
},
removeQuotes() {
if (this.hasQuotes === false) return;
const openIndex = this.sourceIndex;
const closeIndex = openIndex + this.name.length + 2;
this.hasQuotes = false;
decl.value = decl.value.slice(0, openIndex) + this.name + decl.value.substring(closeIndex);
this.resetIndexes(-2);
},
addQuotes() {
if (this.hasQuotes === true) return;
const openIndex = this.sourceIndex;
const closeIndex = openIndex + this.name.length;
this.hasQuotes = true;
const fixedName = `"${this.name}"`;
decl.value = decl.value.slice(0, openIndex) + fixedName + decl.value.substring(closeIndex);
this.resetIndexes(2);
},
};
mutableNodes.push(newNode);
});
return mutableNodes;
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondary, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always-where-required', 'always-where-recommended', 'always-unless-keyword'],
});
if (!validOptions) {
return;
}
root.walkDecls(/^font(-family)?$/i, (decl) => {
if (!isStandardSyntaxValue(decl.value)) {
return;
}
let fontFamilyNodes = makeMutableFontFamilies(findFontFamily(decl.value), decl);
if (fontFamilyNodes.length === 0) {
return;
}
for (const fontFamilyNode of fontFamilyNodes) {
checkFamilyName(fontFamilyNode, decl);
}
});
/**
* @param {MutableNode} fontFamilyNode
* @param {import('postcss').Declaration} decl
*/
function checkFamilyName(fontFamilyNode, decl) {
const { name: family, rawName: rawFamily, hasQuotes } = fontFamilyNode;
if (isVariable(rawFamily)) {
return;
}
// Disallow quotes around (case-insensitive) keywords
// and system font keywords in all cases
if (fontFamilyKeywords.has(family.toLowerCase()) || isSystemFontKeyword(family)) {
if (hasQuotes) {
if (context.fix) {
fontFamilyNode.removeQuotes();
return;
}
return complain(messages.rejected(family), rawFamily, decl);
}
return;
}
const required = quotesRequired(family);
const recommended = quotesRecommended(family);
switch (primary) {
case 'always-unless-keyword':
if (!hasQuotes) {
if (context.fix) {
fontFamilyNode.addQuotes();
return;
}
return complain(messages.expected(family), rawFamily, decl);
}
return;
case 'always-where-recommended':
if (!recommended && hasQuotes) {
if (context.fix) {
fontFamilyNode.removeQuotes();
return;
}
return complain(messages.rejected(family), rawFamily, decl);
}
if (recommended && !hasQuotes) {
if (context.fix) {
fontFamilyNode.addQuotes();
return;
}
return complain(messages.expected(family), rawFamily, decl);
}
return;
case 'always-where-required':
if (!required && hasQuotes) {
if (context.fix) {
fontFamilyNode.removeQuotes();
return;
}
return complain(messages.rejected(family), rawFamily, decl);
}
if (required && !hasQuotes) {
if (context.fix) {
fontFamilyNode.addQuotes();
return;
}
return complain(messages.expected(family), rawFamily, decl);
}
}
}
/**
* @param {string} message
* @param {string} family
* @param {import('postcss').Declaration} decl
*/
function complain(message, family, decl) {
report({
result,
ruleName,
message,
node: decl,
word: family,
});
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,122 @@
'use strict';
const declarationValueIndex = require('../../utils/declarationValueIndex');
const findFontFamily = require('../../utils/findFontFamily');
const { fontFamilyKeywords } = require('../../reference/keywords');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'font-family-no-duplicate-names';
const messages = ruleMessages(ruleName, {
rejected: (name) => `Unexpected duplicate name ${name}`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/font-family-no-duplicate-names',
};
/**
* @param {import('postcss-value-parser').Node} node
*/
const isFamilyNameKeyword = (node) =>
!('quote' in node) && fontFamilyKeywords.has(node.value.toLowerCase());
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{ actual: primary },
{
actual: secondaryOptions,
possible: {
ignoreFontFamilyNames: [isString, isRegExp],
},
optional: true,
},
);
if (!validOptions) {
return;
}
root.walkDecls(/^font(-family)?$/i, (decl) => {
const keywords = new Set();
const familyNames = new Set();
const fontFamilies = findFontFamily(decl.value);
if (fontFamilies.length === 0) {
return;
}
for (const fontFamilyNode of fontFamilies) {
const family = fontFamilyNode.value.trim();
if (optionsMatches(secondaryOptions, 'ignoreFontFamilyNames', family)) {
continue;
}
const rawFamily =
'quote' in fontFamilyNode ? fontFamilyNode.quote + family + fontFamilyNode.quote : family;
if (isFamilyNameKeyword(fontFamilyNode)) {
if (keywords.has(family.toLowerCase())) {
complain(
messages.rejected(family),
declarationValueIndex(decl) + fontFamilyNode.sourceIndex,
rawFamily.length,
decl,
);
continue;
}
keywords.add(family);
continue;
}
if (familyNames.has(family)) {
complain(
messages.rejected(family),
declarationValueIndex(decl) + fontFamilyNode.sourceIndex,
rawFamily.length,
decl,
);
continue;
}
familyNames.add(family);
}
});
/**
* @param {string} message
* @param {number} index
* @param {number} length
* @param {import('postcss').Declaration} decl
*/
function complain(message, index, length, decl) {
report({
result,
ruleName,
message,
node: decl,
index,
endIndex: index + length,
});
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,120 @@
'use strict';
const declarationValueIndex = require('../../utils/declarationValueIndex');
const findFontFamily = require('../../utils/findFontFamily');
const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
const isVariable = require('../../utils/isVariable');
const { systemFontKeywords, fontFamilyKeywords } = require('../../reference/keywords');
const optionsMatches = require('../../utils/optionsMatches');
const postcss = require('postcss');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { isAtRule } = require('../../utils/typeGuards');
const { isRegExp, isString, assert } = require('../../utils/validateTypes');
const ruleName = 'font-family-no-missing-generic-family-keyword';
const messages = ruleMessages(ruleName, {
rejected: 'Unexpected missing generic font family',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/font-family-no-missing-generic-family-keyword',
};
/**
* @param {import('postcss-value-parser').Node} node
* @returns {boolean}
*/
const isFamilyNameKeyword = (node) =>
!('quote' in node) && fontFamilyKeywords.has(node.value.toLowerCase());
/**
* @param {string} value
* @returns {boolean}
*/
const isLastFontFamilyVariable = (value) => {
const lastValue = postcss.list.comma(value).pop();
return lastValue != null && (isVariable(lastValue) || !isStandardSyntaxValue(lastValue));
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{ actual: primary },
{
actual: secondaryOptions,
possible: {
ignoreFontFamilies: [isString, isRegExp],
},
optional: true,
},
);
if (!validOptions) {
return;
}
root.walkDecls(/^font(-family)?$/i, (decl) => {
// Ignore @font-face
const parent = decl.parent;
if (parent && isAtRule(parent) && parent.name.toLowerCase() === 'font-face') {
return;
}
if (decl.prop === 'font' && systemFontKeywords.has(decl.value.toLowerCase())) {
return;
}
if (isLastFontFamilyVariable(decl.value)) {
return;
}
const fontFamilies = findFontFamily(decl.value);
if (fontFamilies.length === 0) {
return;
}
if (fontFamilies.some((node) => isFamilyNameKeyword(node))) {
return;
}
if (
fontFamilies.some((node) =>
optionsMatches(secondaryOptions, 'ignoreFontFamilies', node.value),
)
) {
return;
}
const lastFontFamily = fontFamilies[fontFamilies.length - 1];
assert(lastFontFamily);
const valueIndex = declarationValueIndex(decl);
const index = valueIndex + lastFontFamily.sourceIndex;
const endIndex = valueIndex + lastFontFamily.sourceEndIndex;
report({
result,
ruleName,
message: messages.rejected,
node: decl,
index,
endIndex,
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,214 @@
'use strict';
const valueParser = require('postcss-value-parser');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const isNumbery = require('../../utils/isNumbery');
const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
const isVariable = require('../../utils/isVariable');
const {
fontWeightNonNumericKeywords,
fontWeightRelativeKeywords,
} = require('../../reference/keywords');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const setDeclarationValue = require('../../utils/setDeclarationValue');
const validateOptions = require('../../utils/validateOptions');
const { assertString } = require('../../utils/validateTypes');
const ruleName = 'font-weight-notation';
const messages = ruleMessages(ruleName, {
expected: (type) => `Expected ${type} font-weight notation`,
expectedWithActual: (actual, expected) => `Expected "${actual}" to be "${expected}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/font-weight-notation',
fixable: true,
};
const NORMAL_KEYWORD = 'normal';
const NAMED_TO_NUMERIC = new Map([
['normal', '400'],
['bold', '700'],
]);
const NUMERIC_TO_NAMED = new Map([
['400', 'normal'],
['700', 'bold'],
]);
/** @type {import('stylelint').Rule<'numeric' | 'named-where-possible'>} */
const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: ['numeric', 'named-where-possible'],
},
{
actual: secondaryOptions,
possible: {
ignore: ['relative'],
},
optional: true,
},
);
if (!validOptions) {
return;
}
const ignoreRelative = optionsMatches(secondaryOptions, 'ignore', 'relative');
root.walkDecls(/^font(-weight)?$/i, (decl) => {
const isFontShorthandProp = decl.prop.toLowerCase() === 'font';
const parsedValue = valueParser(getDeclarationValue(decl));
const valueNodes = parsedValue.nodes;
const hasNumericFontWeight = valueNodes.some((node, index, nodes) => {
return isNumbery(node.value) && !isDivNode(nodes[index - 1]);
});
for (const [index, valueNode] of valueNodes.entries()) {
if (!isPossibleFontWeightNode(valueNode, index, valueNodes)) continue;
const { value } = valueNode;
if (isFontShorthandProp) {
if (value.toLowerCase() === NORMAL_KEYWORD && hasNumericFontWeight) {
continue; // Not `normal` for font-weight
}
if (checkWeight(decl, valueNode)) {
break; // Stop traverse if font-weight is processed
}
}
checkWeight(decl, valueNode);
}
if (context.fix) {
// Autofix after the loop ends can prevent value nodes from changing their positions during the loop.
setDeclarationValue(decl, parsedValue.toString());
}
});
/**
* @param {import('postcss').Declaration} decl
* @param {import('postcss-value-parser').Node} weightValueNode
* @returns {true | undefined}
*/
function checkWeight(decl, weightValueNode) {
const weightValue = weightValueNode.value;
if (!isStandardSyntaxValue(weightValue)) {
return;
}
if (isVariable(weightValue)) {
return;
}
const lowerWeightValue = weightValue.toLowerCase();
if (ignoreRelative && fontWeightRelativeKeywords.has(lowerWeightValue)) {
return;
}
if (primary === 'numeric') {
if (!isNumbery(lowerWeightValue) && fontWeightNonNumericKeywords.has(lowerWeightValue)) {
const numericValue = NAMED_TO_NUMERIC.get(lowerWeightValue);
if (context.fix) {
if (numericValue) {
weightValueNode.value = numericValue;
return true;
}
}
const msg = numericValue
? messages.expectedWithActual(weightValue, numericValue)
: messages.expected('numeric');
complain(msg, weightValueNode);
return true;
}
}
if (primary === 'named-where-possible') {
if (isNumbery(lowerWeightValue) && NUMERIC_TO_NAMED.has(lowerWeightValue)) {
const namedValue = NUMERIC_TO_NAMED.get(lowerWeightValue);
assertString(namedValue);
if (context.fix) {
weightValueNode.value = namedValue;
return true;
}
complain(messages.expectedWithActual(weightValue, namedValue), weightValueNode);
return true;
}
}
/**
* @param {string} message
* @param {import('postcss-value-parser').Node} valueNode
*/
function complain(message, valueNode) {
const index = declarationValueIndex(decl) + valueNode.sourceIndex;
const endIndex = index + valueNode.value.length;
report({
ruleName,
result,
message,
node: decl,
index,
endIndex,
});
}
}
};
};
/**
* @param {import('postcss-value-parser').Node | undefined} node
* @returns {boolean}
*/
function isDivNode(node) {
return node !== undefined && node.type === 'div';
}
/**
* @param {import('postcss-value-parser').Node} node
* @param {number} index
* @param {import('postcss-value-parser').Node[]} nodes
* @returns {boolean}
*/
function isPossibleFontWeightNode(node, index, nodes) {
if (node.type !== 'word') return false;
// Exclude `<font-size>/<line-height>` format like `16px/3`.
if (isDivNode(nodes[index - 1])) return false;
if (isDivNode(nodes[index + 1])) return false;
return true;
}
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,70 @@
'use strict';
const declarationValueIndex = require('../../utils/declarationValueIndex');
const isStandardSyntaxFunction = require('../../utils/isStandardSyntaxFunction');
const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const valueParser = require('postcss-value-parser');
const vendor = require('../../utils/vendor');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'function-allowed-list';
const messages = ruleMessages(ruleName, {
rejected: (name) => `Unexpected function "${name}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-allowed-list',
};
/** @type {import('stylelint').Rule<string | RegExp | Array<string | RegExp>>} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [isString, isRegExp],
});
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
valueParser(decl.value).walk((node) => {
if (node.type !== 'function') {
return;
}
if (!isStandardSyntaxFunction(node)) {
return;
}
if (matchesStringOrRegExp(vendor.unprefixed(node.value), primary)) {
return;
}
const index = declarationValueIndex(decl) + node.sourceIndex;
const endIndex = index + node.value.length;
report({
message: messages.rejected(node.value),
node: decl,
index,
endIndex,
result,
ruleName,
});
});
});
};
};
rule.primaryOptionArray = true;
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,374 @@
'use strict';
const valueParser = require('postcss-value-parser');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const setDeclarationValue = require('../../utils/setDeclarationValue');
const validateOptions = require('../../utils/validateOptions');
const { assert } = require('../../utils/validateTypes');
const ruleName = 'function-calc-no-unspaced-operator';
const messages = ruleMessages(ruleName, {
expectedBefore: (operator) => `Expected single space before "${operator}" operator`,
expectedAfter: (operator) => `Expected single space after "${operator}" operator`,
expectedOperatorBeforeSign: (operator) => `Expected an operator before sign "${operator}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-calc-no-unspaced-operator',
fixable: true,
};
const OPERATORS = new Set(['+', '-']);
const OPERATOR_REGEX = /[+-]/;
const ALL_OPERATORS = new Set([...OPERATORS, '*', '/']);
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual: primary });
if (!validOptions) return;
/**
* @param {string} message
* @param {import('postcss').Node} node
* @param {number} index
* @param {string} operator
*/
function complain(message, node, index, operator) {
const endIndex = index + operator.length;
report({ message, node, index, endIndex, result, ruleName });
}
root.walkDecls((decl) => {
let needsFix = false;
const valueIndex = declarationValueIndex(decl);
const parsedValue = valueParser(getDeclarationValue(decl));
/**
* @param {import('postcss-value-parser').Node} operatorNode
* @param {import('postcss-value-parser').Node} currentNode
* @param {boolean} isBeforeOp
*/
function checkAroundOperator(operatorNode, currentNode, isBeforeOp) {
const operator = operatorNode.value;
const operatorSourceIndex = operatorNode.sourceIndex;
if (currentNode && !isSingleSpace(currentNode)) {
if (currentNode.type === 'word') {
if (isBeforeOp) {
const lastChar = currentNode.value.slice(-1);
if (OPERATORS.has(lastChar)) {
if (context.fix) {
currentNode.value = `${currentNode.value.slice(0, -1)} ${lastChar}`;
return true;
}
complain(
messages.expectedOperatorBeforeSign(operator),
decl,
operatorSourceIndex,
operator,
);
return true;
}
} else {
const firstChar = currentNode.value.slice(0, 1);
if (OPERATORS.has(firstChar)) {
if (context.fix) {
currentNode.value = `${firstChar} ${currentNode.value.slice(1)}`;
return true;
}
complain(messages.expectedAfter(operator), decl, operatorSourceIndex, operator);
return true;
}
}
if (context.fix) {
needsFix = true;
currentNode.value = isBeforeOp ? `${currentNode.value} ` : ` ${currentNode.value}`;
return true;
}
complain(
isBeforeOp ? messages.expectedBefore(operator) : messages.expectedAfter(operator),
decl,
valueIndex + operatorSourceIndex,
operator,
);
return true;
}
if (currentNode.type === 'space') {
const indexOfFirstNewLine = currentNode.value.search(/(\n|\r\n)/);
if (indexOfFirstNewLine === 0) return;
if (context.fix) {
needsFix = true;
currentNode.value =
indexOfFirstNewLine === -1 ? ' ' : currentNode.value.slice(indexOfFirstNewLine);
return true;
}
const message = isBeforeOp
? messages.expectedBefore(operator)
: messages.expectedAfter(operator);
complain(message, decl, valueIndex + operatorSourceIndex, operator);
return true;
}
if (currentNode.type === 'function') {
if (context.fix) {
needsFix = true;
currentNode.value = isBeforeOp ? `${currentNode.value} ` : ` ${currentNode.value}`;
return true;
}
const message = isBeforeOp
? messages.expectedBefore(operator)
: messages.expectedAfter(operator);
complain(message, decl, valueIndex + operatorSourceIndex, operator);
return true;
}
}
return false;
}
/**
* @param {import('postcss-value-parser').Node[]} nodes
*/
function checkForOperatorInFirstNode(nodes) {
const firstNode = nodes[0];
assert(firstNode);
if (firstNode.type !== 'word') return false;
if (!isStandardSyntaxValue(firstNode.value)) return false;
const operatorIndex = firstNode.value.search(OPERATOR_REGEX);
const operator = firstNode.value.slice(operatorIndex, operatorIndex + 1);
if (operatorIndex <= 0) return false;
const charBefore = firstNode.value.charAt(operatorIndex - 1);
const charAfter = firstNode.value.charAt(operatorIndex + 1);
if (charBefore && charBefore !== ' ' && charAfter && charAfter !== ' ') {
if (context.fix) {
needsFix = true;
firstNode.value = insertCharAtIndex(firstNode.value, operatorIndex + 1, ' ');
firstNode.value = insertCharAtIndex(firstNode.value, operatorIndex, ' ');
} else {
complain(
messages.expectedBefore(operator),
decl,
valueIndex + firstNode.sourceIndex + operatorIndex,
operator,
);
complain(
messages.expectedAfter(operator),
decl,
valueIndex + firstNode.sourceIndex + operatorIndex + 1,
operator,
);
}
} else if (charBefore && charBefore !== ' ') {
if (context.fix) {
needsFix = true;
firstNode.value = insertCharAtIndex(firstNode.value, operatorIndex, ' ');
} else {
complain(
messages.expectedBefore(operator),
decl,
valueIndex + firstNode.sourceIndex + operatorIndex,
operator,
);
}
} else if (charAfter && charAfter !== ' ') {
if (context.fix) {
needsFix = true;
firstNode.value = insertCharAtIndex(firstNode.value, operatorIndex, ' ');
} else {
complain(
messages.expectedAfter(operator),
decl,
valueIndex + firstNode.sourceIndex + operatorIndex + 1,
operator,
);
}
}
return true;
}
/**
* @param {import('postcss-value-parser').Node[]} nodes
*/
function checkForOperatorInLastNode(nodes) {
if (nodes.length === 1) return false;
const lastNode = nodes[nodes.length - 1];
assert(lastNode);
if (lastNode.type !== 'word') return false;
const operatorIndex = lastNode.value.search(OPERATOR_REGEX);
if (operatorIndex === -1) return false;
if (lastNode.value.charAt(operatorIndex - 1) === ' ') return false;
// E.g. "10px * -2" when the last node is "-2"
if (
isOperator(nodes[nodes.length - 3], ALL_OPERATORS) &&
isSingleSpace(nodes[nodes.length - 2])
) {
return false;
}
if (context.fix) {
needsFix = true;
lastNode.value = insertCharAtIndex(lastNode.value, operatorIndex + 1, ' ').trim();
lastNode.value = insertCharAtIndex(lastNode.value, operatorIndex, ' ').trim();
return true;
}
const operator = lastNode.value.charAt(operatorIndex);
complain(
messages.expectedOperatorBeforeSign(operator),
decl,
valueIndex + lastNode.sourceIndex + operatorIndex,
operator,
);
return true;
}
/**
* @param {import('postcss-value-parser').Node[]} nodes
*/
function checkWords(nodes) {
if (checkForOperatorInFirstNode(nodes) || checkForOperatorInLastNode(nodes)) return;
for (const [index, node] of nodes.entries()) {
const lastChar = node.value.slice(-1);
const firstChar = node.value.slice(0, 1);
if (node.type === 'word') {
if (index === 0 && OPERATORS.has(lastChar)) {
if (context.fix) {
node.value = `${node.value.slice(0, -1)} ${lastChar}`;
continue;
}
complain(messages.expectedBefore(lastChar), decl, node.sourceIndex, lastChar);
} else if (index === nodes.length && OPERATORS.has(firstChar)) {
if (context.fix) {
node.value = `${firstChar} ${node.value.slice(1)}`;
continue;
}
complain(
messages.expectedOperatorBeforeSign(firstChar),
decl,
node.sourceIndex,
firstChar,
);
}
}
}
}
parsedValue.walk((node) => {
if (node.type !== 'function' || node.value.toLowerCase() !== 'calc') return;
const { nodes } = node;
let foundOperatorNode = false;
for (const [nodeIndex, currNode] of nodes.entries()) {
if (!isOperator(currNode)) continue;
foundOperatorNode = true;
const nodeBefore = nodes[nodeIndex - 1];
const nodeAfter = nodes[nodeIndex + 1];
if (isSingleSpace(nodeBefore) && isSingleSpace(nodeAfter)) continue;
if (nodeAfter && checkAroundOperator(currNode, nodeAfter, false)) continue;
nodeBefore && checkAroundOperator(currNode, nodeBefore, true);
}
if (!foundOperatorNode) {
checkWords(nodes);
}
});
if (needsFix) {
setDeclarationValue(decl, parsedValue.toString());
}
});
};
};
/**
* @param {string} str
* @param {number} index
* @param {string} char
*/
function insertCharAtIndex(str, index, char) {
return str.slice(0, index) + char + str.slice(index, str.length);
}
/**
* @param {import('postcss-value-parser').Node | undefined} node
* @returns {node is import('postcss-value-parser').SpaceNode & { value: ' ' } }
*/
function isSingleSpace(node) {
return node != null && node.type === 'space' && node.value === ' ';
}
/**
* @param {import('postcss-value-parser').Node | undefined} node
* @param {Set<string>} [operators]
* @returns {node is import('postcss-value-parser').WordNode}
*/
function isOperator(node, operators = OPERATORS) {
return node != null && node.type === 'word' && operators.has(node.value);
}
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,59 @@
'use strict';
const fixer = require('../functionCommaSpaceFix');
const functionCommaSpaceChecker = require('../functionCommaSpaceChecker');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'function-comma-newline-after';
const messages = ruleMessages(ruleName, {
expectedAfter: () => 'Expected newline after ","',
expectedAfterMultiLine: () => 'Expected newline after "," in a multi-line function',
rejectedAfterMultiLine: () => 'Unexpected whitespace after "," in a multi-line function',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-comma-newline-after',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
const checker = whitespaceChecker('newline', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'always-multi-line', 'never-multi-line'],
});
if (!validOptions) {
return;
}
functionCommaSpaceChecker({
root,
result,
locationChecker: checker.afterOneOnly,
checkedRuleName: ruleName,
fix: context.fix
? (div, index, nodes) =>
fixer({
div,
index,
nodes,
expectation: primary,
position: 'after',
symb: context.newline || '',
})
: null,
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,59 @@
'use strict';
const fixer = require('../functionCommaSpaceFix');
const functionCommaSpaceChecker = require('../functionCommaSpaceChecker');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'function-comma-newline-before';
const messages = ruleMessages(ruleName, {
expectedBefore: () => 'Expected newline before ","',
expectedBeforeMultiLine: () => 'Expected newline before "," in a multi-line function',
rejectedBeforeMultiLine: () => 'Unexpected whitespace before "," in a multi-line function',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-comma-newline-before',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
const checker = whitespaceChecker('newline', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'always-multi-line', 'never-multi-line'],
});
if (!validOptions) {
return;
}
functionCommaSpaceChecker({
root,
result,
locationChecker: checker.beforeAllowingIndentation,
checkedRuleName: ruleName,
fix: context.fix
? (div, index, nodes) =>
fixer({
div,
index,
nodes,
expectation: primary,
position: 'before',
symb: context.newline || '',
})
: null,
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,60 @@
'use strict';
const fixer = require('../functionCommaSpaceFix');
const functionCommaSpaceChecker = require('../functionCommaSpaceChecker');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'function-comma-space-after';
const messages = ruleMessages(ruleName, {
expectedAfter: () => 'Expected single space after ","',
rejectedAfter: () => 'Unexpected whitespace after ","',
expectedAfterSingleLine: () => 'Expected single space after "," in a single-line function',
rejectedAfterSingleLine: () => 'Unexpected whitespace after "," in a single-line function',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-comma-space-after',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
const checker = whitespaceChecker('space', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'never', 'always-single-line', 'never-single-line'],
});
if (!validOptions) {
return;
}
functionCommaSpaceChecker({
root,
result,
locationChecker: checker.after,
checkedRuleName: ruleName,
fix: context.fix
? (div, index, nodes) =>
fixer({
div,
index,
nodes,
expectation: primary,
position: 'after',
symb: ' ',
})
: null,
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,60 @@
'use strict';
const fixer = require('../functionCommaSpaceFix');
const functionCommaSpaceChecker = require('../functionCommaSpaceChecker');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const whitespaceChecker = require('../../utils/whitespaceChecker');
const ruleName = 'function-comma-space-before';
const messages = ruleMessages(ruleName, {
expectedBefore: () => 'Expected single space before ","',
rejectedBefore: () => 'Unexpected whitespace before ","',
expectedBeforeSingleLine: () => 'Expected single space before "," in a single-line function',
rejectedBeforeSingleLine: () => 'Unexpected whitespace before "," in a single-line function',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-comma-space-before',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
const checker = whitespaceChecker('space', primary, messages);
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'never', 'always-single-line', 'never-single-line'],
});
if (!validOptions) {
return;
}
functionCommaSpaceChecker({
root,
result,
locationChecker: checker.before,
checkedRuleName: ruleName,
fix: context.fix
? (div, index, nodes) =>
fixer({
div,
index,
nodes,
expectation: primary,
position: 'before',
symb: ' ',
})
: null,
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,71 @@
'use strict';
const declarationValueIndex = require('../../utils/declarationValueIndex');
const isStandardSyntaxFunction = require('../../utils/isStandardSyntaxFunction');
const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const valueParser = require('postcss-value-parser');
const vendor = require('../../utils/vendor');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'function-disallowed-list';
const messages = ruleMessages(ruleName, {
rejected: (name) => `Unexpected function "${name}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-disallowed-list',
};
/** @type {import('stylelint').Rule<string | RegExp | Array<string | RegExp>>} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [isString, isRegExp],
});
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
valueParser(decl.value).walk((node) => {
if (node.type !== 'function') {
return;
}
if (!isStandardSyntaxFunction(node)) {
return;
}
if (!matchesStringOrRegExp(vendor.unprefixed(node.value), primary)) {
return;
}
const index = declarationValueIndex(decl) + node.sourceIndex;
const endIndex = index + node.value.length;
report({
message: messages.rejected,
messageArgs: [node.value],
node: decl,
index,
endIndex,
result,
ruleName,
});
});
});
};
};
rule.primaryOptionArray = true;
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,123 @@
'use strict';
const declarationValueIndex = require('../../utils/declarationValueIndex');
const functionArgumentsSearch = require('../../utils/functionArgumentsSearch');
const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const valueParser = require('postcss-value-parser');
const vendor = require('../../utils/vendor');
const ruleName = 'function-linear-gradient-no-nonstandard-direction';
const messages = ruleMessages(ruleName, {
rejected: 'Unexpected nonstandard direction',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-linear-gradient-no-nonstandard-direction',
};
/**
* @param {string} source
* @param {boolean} withToPrefix
*/
function isStandardDirection(source, withToPrefix) {
const regexp = withToPrefix
? /^to (top|left|bottom|right)(?: (top|left|bottom|right))?$/
: /^(top|left|bottom|right)(?: (top|left|bottom|right))?$/;
const matches = source.match(regexp);
if (!matches) {
return false;
}
if (matches.length === 2) {
return true;
}
// Cannot repeat side-or-corner, e.g. "to top top"
if (matches.length === 3 && matches[1] !== matches[2]) {
return true;
}
return false;
}
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual: primary });
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
valueParser(decl.value).walk((valueNode) => {
if (valueNode.type !== 'function') {
return;
}
functionArgumentsSearch(
valueParser.stringify(valueNode).toLowerCase(),
/^(-webkit-|-moz-|-o-|-ms-)?linear-gradient$/i,
(expression, expressionIndex) => {
const args = expression.split(',');
const firstArg = (args[0] || '').trim();
// If the first arg is not standard, return early
if (!isStandardSyntaxValue(firstArg)) {
return;
}
// If the first character is a number, we can assume the user intends an angle
if (/[\d.]/.test(firstArg.charAt(0))) {
if (/^[\d.]+(?:deg|grad|rad|turn)$/.test(firstArg)) {
return;
}
complain();
return;
}
// The first argument may not be a direction: it may be an angle,
// or a color stop (in which case user gets default direction, "to bottom")
// cf. https://drafts.csswg.org/css-images-3/#linear-gradient-syntax
if (!/left|right|top|bottom/.test(firstArg)) {
return;
}
const withToPrefix = !vendor.prefix(valueNode.value);
if (!isStandardDirection(firstArg, withToPrefix)) {
complain();
}
function complain() {
const index = declarationValueIndex(decl) + valueNode.sourceIndex + expressionIndex;
const endIndex = index + (args[0] || '').trimEnd().length;
report({
message: messages.rejected,
node: decl,
index,
endIndex,
result,
ruleName,
});
}
},
);
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,112 @@
'use strict';
const getDeclarationValue = require('../../utils/getDeclarationValue');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const setDeclarationValue = require('../../utils/setDeclarationValue');
const validateOptions = require('../../utils/validateOptions');
const valueParser = require('postcss-value-parser');
const { isNumber } = require('../../utils/validateTypes');
const ruleName = 'function-max-empty-lines';
const messages = ruleMessages(ruleName, {
expected: (max) => `Expected no more than ${max} empty ${max === 1 ? 'line' : 'lines'}`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-max-empty-lines',
fixable: true,
};
/**
* @param {import('postcss').Declaration} decl
*/
function placeIndexOnValueStart(decl) {
if (decl.raws.between == null) throw new Error('`between` must be present');
return decl.prop.length + decl.raws.between.length - 1;
}
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
const maxAdjacentNewlines = primary + 1;
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: isNumber,
});
if (!validOptions) {
return;
}
const violatedCRLFNewLinesRegex = new RegExp(`(?:\r\n){${maxAdjacentNewlines + 1},}`);
const violatedLFNewLinesRegex = new RegExp(`\n{${maxAdjacentNewlines + 1},}`);
const allowedLFNewLinesString = context.fix ? '\n'.repeat(maxAdjacentNewlines) : '';
const allowedCRLFNewLinesString = context.fix ? '\r\n'.repeat(maxAdjacentNewlines) : '';
root.walkDecls((decl) => {
if (!decl.value.includes('(')) {
return;
}
const stringValue = getDeclarationValue(decl);
/** @type {Array<[string, string]>} */
const splittedValue = [];
let sourceIndexStart = 0;
valueParser(stringValue).walk((node) => {
if (
node.type !== 'function' /* ignore non functions */ ||
node.value.length === 0 /* ignore sass lists */
) {
return;
}
const stringifiedNode = valueParser.stringify(node);
if (
!violatedLFNewLinesRegex.test(stringifiedNode) &&
!violatedCRLFNewLinesRegex.test(stringifiedNode)
) {
return;
}
if (context.fix) {
const newNodeString = stringifiedNode
.replace(new RegExp(violatedLFNewLinesRegex, 'gm'), allowedLFNewLinesString)
.replace(new RegExp(violatedCRLFNewLinesRegex, 'gm'), allowedCRLFNewLinesString);
splittedValue.push([
stringValue.slice(sourceIndexStart, node.sourceIndex),
newNodeString,
]);
sourceIndexStart = node.sourceIndex + stringifiedNode.length;
} else {
report({
message: messages.expected(primary),
node: decl,
index: placeIndexOnValueStart(decl) + node.sourceIndex,
result,
ruleName,
});
}
});
if (context.fix && splittedValue.length > 0) {
const updatedValue =
splittedValue.reduce((acc, curr) => acc + curr[0] + curr[1], '') +
stringValue.slice(sourceIndexStart);
setDeclarationValue(decl, updatedValue);
}
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,114 @@
'use strict';
const declarationValueIndex = require('../../utils/declarationValueIndex');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const isStandardSyntaxFunction = require('../../utils/isStandardSyntaxFunction');
const { camelCaseFunctions } = require('../../reference/functions');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const setDeclarationValue = require('../../utils/setDeclarationValue');
const validateOptions = require('../../utils/validateOptions');
const valueParser = require('postcss-value-parser');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'function-name-case';
const messages = ruleMessages(ruleName, {
expected: (actual, expected) => `Expected "${actual}" to be "${expected}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-name-case',
fixable: true,
};
const mapLowercaseFunctionNamesToCamelCase = new Map();
for (const func of camelCaseFunctions) {
mapLowercaseFunctionNamesToCamelCase.set(func.toLowerCase(), func);
}
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: ['lower', 'upper'],
},
{
actual: secondaryOptions,
possible: {
ignoreFunctions: [isString, isRegExp],
},
optional: true,
},
);
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
let needFix = false;
const parsed = valueParser(getDeclarationValue(decl));
parsed.walk((node) => {
if (node.type !== 'function' || !isStandardSyntaxFunction(node)) {
return;
}
const functionName = node.value;
const functionNameLowerCase = functionName.toLowerCase();
if (optionsMatches(secondaryOptions, 'ignoreFunctions', functionName)) {
return;
}
let expectedFunctionName = null;
if (
primary === 'lower' &&
mapLowercaseFunctionNamesToCamelCase.has(functionNameLowerCase)
) {
expectedFunctionName = mapLowercaseFunctionNamesToCamelCase.get(functionNameLowerCase);
} else if (primary === 'lower') {
expectedFunctionName = functionNameLowerCase;
} else {
expectedFunctionName = functionName.toUpperCase();
}
if (functionName === expectedFunctionName) {
return;
}
if (context.fix) {
needFix = true;
node.value = expectedFunctionName;
return;
}
report({
message: messages.expected(functionName, expectedFunctionName),
node: decl,
index: declarationValueIndex(decl) + node.sourceIndex,
result,
ruleName,
});
});
if (context.fix && needFix) {
setDeclarationValue(decl, parsed.toString());
}
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,99 @@
'use strict';
const fs = require('fs');
const valueParser = require('postcss-value-parser');
const functionsListPath = require('css-functions-list');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const isStandardSyntaxFunction = require('../../utils/isStandardSyntaxFunction');
const isCustomFunction = require('../../utils/isCustomFunction');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'function-no-unknown';
const messages = ruleMessages(ruleName, {
rejected: (name) => `Unexpected unknown function "${name}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-no-unknown',
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{ actual: primary },
{
actual: secondaryOptions,
possible: {
ignoreFunctions: [isString, isRegExp],
},
optional: true,
},
);
if (!validOptions) {
return;
}
const functionsList = [
...JSON.parse(fs.readFileSync(functionsListPath.toString(), 'utf8')),
// #5960
'-webkit-gradient',
'color-stop',
'from',
'to',
// #6537
'scroll',
];
root.walkDecls((decl) => {
const { value } = decl;
valueParser(value).walk((node) => {
const name = node.value;
if (node.type !== 'function') {
return;
}
if (!isStandardSyntaxFunction(node)) {
return;
}
if (isCustomFunction(name)) {
return;
}
if (optionsMatches(secondaryOptions, 'ignoreFunctions', name)) {
return;
}
if (functionsList.includes(name.toLowerCase())) {
return;
}
report({
message: messages.rejected(name),
node: decl,
index: declarationValueIndex(decl) + node.sourceIndex,
result,
ruleName,
word: name,
});
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,275 @@
'use strict';
const declarationValueIndex = require('../../utils/declarationValueIndex');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const isSingleLineString = require('../../utils/isSingleLineString');
const isStandardSyntaxFunction = require('../../utils/isStandardSyntaxFunction');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const setDeclarationValue = require('../../utils/setDeclarationValue');
const validateOptions = require('../../utils/validateOptions');
const valueParser = require('postcss-value-parser');
const ruleName = 'function-parentheses-newline-inside';
const messages = ruleMessages(ruleName, {
expectedOpening: 'Expected newline after "("',
expectedClosing: 'Expected newline before ")"',
expectedOpeningMultiLine: 'Expected newline after "(" in a multi-line function',
rejectedOpeningMultiLine: 'Unexpected whitespace after "(" in a multi-line function',
expectedClosingMultiLine: 'Expected newline before ")" in a multi-line function',
rejectedClosingMultiLine: 'Unexpected whitespace before ")" in a multi-line function',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-parentheses-newline-inside',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'always-multi-line', 'never-multi-line'],
});
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
if (!decl.value.includes('(')) {
return;
}
let hasFixed = false;
const declValue = getDeclarationValue(decl);
const parsedValue = valueParser(declValue);
parsedValue.walk((valueNode) => {
if (valueNode.type !== 'function') {
return;
}
if (!isStandardSyntaxFunction(valueNode)) {
return;
}
const functionString = valueParser.stringify(valueNode);
const isMultiLine = !isSingleLineString(functionString);
const containsNewline = (/** @type {string} */ str) => str.includes('\n');
// Check opening ...
const openingIndex = valueNode.sourceIndex + valueNode.value.length + 1;
const checkBefore = getCheckBefore(valueNode);
if (primary === 'always' && !containsNewline(checkBefore)) {
if (context.fix) {
hasFixed = true;
fixBeforeForAlways(valueNode, context.newline || '');
} else {
complain(messages.expectedOpening, openingIndex);
}
}
if (isMultiLine && primary === 'always-multi-line' && !containsNewline(checkBefore)) {
if (context.fix) {
hasFixed = true;
fixBeforeForAlways(valueNode, context.newline || '');
} else {
complain(messages.expectedOpeningMultiLine, openingIndex);
}
}
if (isMultiLine && primary === 'never-multi-line' && checkBefore !== '') {
if (context.fix) {
hasFixed = true;
fixBeforeForNever(valueNode);
} else {
complain(messages.rejectedOpeningMultiLine, openingIndex);
}
}
// Check closing ...
const closingIndex = valueNode.sourceIndex + functionString.length - 2;
const checkAfter = getCheckAfter(valueNode);
if (primary === 'always' && !containsNewline(checkAfter)) {
if (context.fix) {
hasFixed = true;
fixAfterForAlways(valueNode, context.newline || '');
} else {
complain(messages.expectedClosing, closingIndex);
}
}
if (isMultiLine && primary === 'always-multi-line' && !containsNewline(checkAfter)) {
if (context.fix) {
hasFixed = true;
fixAfterForAlways(valueNode, context.newline || '');
} else {
complain(messages.expectedClosingMultiLine, closingIndex);
}
}
if (isMultiLine && primary === 'never-multi-line' && checkAfter !== '') {
if (context.fix) {
hasFixed = true;
fixAfterForNever(valueNode);
} else {
complain(messages.rejectedClosingMultiLine, closingIndex);
}
}
});
if (hasFixed) {
setDeclarationValue(decl, parsedValue.toString());
}
/**
* @param {string} message
* @param {number} offset
*/
function complain(message, offset) {
report({
ruleName,
result,
message,
node: decl,
index: declarationValueIndex(decl) + offset,
});
}
});
};
};
/** @typedef {import('postcss-value-parser').FunctionNode} FunctionNode */
/**
* @param {FunctionNode} valueNode
*/
function getCheckBefore(valueNode) {
let before = valueNode.before;
for (const node of valueNode.nodes) {
if (node.type === 'comment') {
continue;
}
if (node.type === 'space') {
before += node.value;
continue;
}
break;
}
return before;
}
/**
* @param {FunctionNode} valueNode
*/
function getCheckAfter(valueNode) {
let after = '';
for (const node of [...valueNode.nodes].reverse()) {
if (node.type === 'comment') {
continue;
}
if (node.type === 'space') {
after = node.value + after;
continue;
}
break;
}
after += valueNode.after;
return after;
}
/**
* @param {FunctionNode} valueNode
* @param {string} newline
*/
function fixBeforeForAlways(valueNode, newline) {
let target;
for (const node of valueNode.nodes) {
if (node.type === 'comment') {
continue;
}
if (node.type === 'space') {
target = node;
continue;
}
break;
}
if (target) {
target.value = newline + target.value;
} else {
valueNode.before = newline + valueNode.before;
}
}
/**
* @param {FunctionNode} valueNode
*/
function fixBeforeForNever(valueNode) {
valueNode.before = '';
for (const node of valueNode.nodes) {
if (node.type === 'comment') {
continue;
}
if (node.type === 'space') {
node.value = '';
continue;
}
break;
}
}
/**
* @param {FunctionNode} valueNode
* @param {string} newline
*/
function fixAfterForAlways(valueNode, newline) {
valueNode.after = newline + valueNode.after;
}
/**
* @param {FunctionNode} valueNode
*/
function fixAfterForNever(valueNode) {
valueNode.after = '';
for (const node of [...valueNode.nodes].reverse()) {
if (node.type === 'comment') {
continue;
}
if (node.type === 'space') {
node.value = '';
continue;
}
break;
}
}
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,174 @@
'use strict';
const declarationValueIndex = require('../../utils/declarationValueIndex');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const isSingleLineString = require('../../utils/isSingleLineString');
const isStandardSyntaxFunction = require('../../utils/isStandardSyntaxFunction');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const setDeclarationValue = require('../../utils/setDeclarationValue');
const validateOptions = require('../../utils/validateOptions');
const valueParser = require('postcss-value-parser');
const ruleName = 'function-parentheses-space-inside';
const messages = ruleMessages(ruleName, {
expectedOpening: 'Expected single space after "("',
rejectedOpening: 'Unexpected whitespace after "("',
expectedClosing: 'Expected single space before ")"',
rejectedClosing: 'Unexpected whitespace before ")"',
expectedOpeningSingleLine: 'Expected single space after "(" in a single-line function',
rejectedOpeningSingleLine: 'Unexpected whitespace after "(" in a single-line function',
expectedClosingSingleLine: 'Expected single space before ")" in a single-line function',
rejectedClosingSingleLine: 'Unexpected whitespace before ")" in a single-line function',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-parentheses-space-inside',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'never', 'always-single-line', 'never-single-line'],
});
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
if (!decl.value.includes('(')) {
return;
}
let hasFixed = false;
const declValue = getDeclarationValue(decl);
const parsedValue = valueParser(declValue);
parsedValue.walk((valueNode) => {
if (valueNode.type !== 'function') {
return;
}
if (!isStandardSyntaxFunction(valueNode)) {
return;
}
// Ignore function without parameters
if (!valueNode.nodes.length) {
return;
}
const functionString = valueParser.stringify(valueNode);
const isSingleLine = isSingleLineString(functionString);
// Check opening ...
const openingIndex = valueNode.sourceIndex + valueNode.value.length + 1;
if (primary === 'always' && valueNode.before !== ' ') {
if (context.fix) {
hasFixed = true;
valueNode.before = ' ';
} else {
complain(messages.expectedOpening, openingIndex);
}
}
if (primary === 'never' && valueNode.before !== '') {
if (context.fix) {
hasFixed = true;
valueNode.before = '';
} else {
complain(messages.rejectedOpening, openingIndex);
}
}
if (isSingleLine && primary === 'always-single-line' && valueNode.before !== ' ') {
if (context.fix) {
hasFixed = true;
valueNode.before = ' ';
} else {
complain(messages.expectedOpeningSingleLine, openingIndex);
}
}
if (isSingleLine && primary === 'never-single-line' && valueNode.before !== '') {
if (context.fix) {
hasFixed = true;
valueNode.before = '';
} else {
complain(messages.rejectedOpeningSingleLine, openingIndex);
}
}
// Check closing ...
const closingIndex = valueNode.sourceIndex + functionString.length - 2;
if (primary === 'always' && valueNode.after !== ' ') {
if (context.fix) {
hasFixed = true;
valueNode.after = ' ';
} else {
complain(messages.expectedClosing, closingIndex);
}
}
if (primary === 'never' && valueNode.after !== '') {
if (context.fix) {
hasFixed = true;
valueNode.after = '';
} else {
complain(messages.rejectedClosing, closingIndex);
}
}
if (isSingleLine && primary === 'always-single-line' && valueNode.after !== ' ') {
if (context.fix) {
hasFixed = true;
valueNode.after = ' ';
} else {
complain(messages.expectedClosingSingleLine, closingIndex);
}
}
if (isSingleLine && primary === 'never-single-line' && valueNode.after !== '') {
if (context.fix) {
hasFixed = true;
valueNode.after = '';
} else {
complain(messages.rejectedClosingSingleLine, closingIndex);
}
}
});
if (hasFixed) {
setDeclarationValue(decl, parsedValue.toString());
}
/**
* @param {string} message
* @param {number} offset
*/
function complain(message, offset) {
report({
ruleName,
result,
message,
node: decl,
index: declarationValueIndex(decl) + offset,
});
}
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,52 @@
'use strict';
const functionArgumentsSearch = require('../../utils/functionArgumentsSearch');
const isStandardSyntaxUrl = require('../../utils/isStandardSyntaxUrl');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'function-url-no-scheme-relative';
const messages = ruleMessages(ruleName, {
rejected: 'Unexpected scheme-relative url',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-url-no-scheme-relative',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual: primary });
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
functionArgumentsSearch(decl.toString().toLowerCase(), 'url', (args, index) => {
const url = args.trim().replace(/^['"]+|['"]+$/g, '');
if (!isStandardSyntaxUrl(url) || !url.startsWith('//')) {
return;
}
report({
message: messages.rejected,
node: decl,
index,
endIndex: index + args.length,
result,
ruleName,
});
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,137 @@
'use strict';
const atRuleParamIndex = require('../../utils/atRuleParamIndex');
const functionArgumentsSearch = require('../../utils/functionArgumentsSearch');
const isStandardSyntaxUrl = require('../../utils/isStandardSyntaxUrl');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'function-url-quotes';
const messages = ruleMessages(ruleName, {
expected: (functionName) => `Expected quotes around "${functionName}" function argument`,
rejected: (functionName) => `Unexpected quotes around "${functionName}" function argument`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-url-quotes',
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: ['always', 'never'],
},
{
actual: secondaryOptions,
possible: {
except: ['empty'],
},
optional: true,
},
);
if (!validOptions) {
return;
}
root.walkAtRules(checkAtRuleParams);
root.walkDecls(checkDeclParams);
/**
* @param {import('postcss').Declaration} decl
*/
function checkDeclParams(decl) {
functionArgumentsSearch(decl.toString().toLowerCase(), 'url', (args, index) => {
checkArgs(args, decl, index, 'url');
});
}
/**
* @param {import('postcss').AtRule} atRule
*/
function checkAtRuleParams(atRule) {
const atRuleParamsLowerCase = atRule.params.toLowerCase();
functionArgumentsSearch(atRuleParamsLowerCase, 'url', (args, index) => {
checkArgs(args, atRule, index + atRuleParamIndex(atRule), 'url');
});
functionArgumentsSearch(atRuleParamsLowerCase, 'url-prefix', (args, index) => {
checkArgs(args, atRule, index + atRuleParamIndex(atRule), 'url-prefix');
});
functionArgumentsSearch(atRuleParamsLowerCase, 'domain', (args, index) => {
checkArgs(args, atRule, index + atRuleParamIndex(atRule), 'domain');
});
}
/**
* @param {string} args
* @param {import('postcss').Node} node
* @param {number} index
* @param {string} functionName
*/
function checkArgs(args, node, index, functionName) {
let shouldHasQuotes = primary === 'always';
const leftTrimmedArgs = args.trimStart();
if (!isStandardSyntaxUrl(leftTrimmedArgs)) {
return;
}
const complaintIndex = index + args.length - leftTrimmedArgs.length;
const complaintEndIndex = index + args.length;
const hasQuotes = leftTrimmedArgs.startsWith("'") || leftTrimmedArgs.startsWith('"');
const trimmedArg = args.trim();
const isEmptyArgument = ['', "''", '""'].includes(trimmedArg);
if (optionsMatches(secondaryOptions, 'except', 'empty') && isEmptyArgument) {
shouldHasQuotes = !shouldHasQuotes;
}
if (shouldHasQuotes) {
if (hasQuotes) {
return;
}
complain(messages.expected(functionName), node, complaintIndex, complaintEndIndex);
} else {
if (!hasQuotes) {
return;
}
complain(messages.rejected(functionName), node, complaintIndex, complaintEndIndex);
}
}
/**
* @param {string} message
* @param {import('postcss').Node} node
* @param {number} index
* @param {number} endIndex
*/
function complain(message, node, index, endIndex) {
report({
message,
node,
index,
endIndex,
result,
ruleName,
});
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,71 @@
'use strict';
const functionArgumentsSearch = require('../../utils/functionArgumentsSearch');
const getSchemeFromUrl = require('../../utils/getSchemeFromUrl');
const isStandardSyntaxUrl = require('../../utils/isStandardSyntaxUrl');
const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'function-url-scheme-allowed-list';
const messages = ruleMessages(ruleName, {
rejected: (scheme) => `Unexpected URL scheme "${scheme}:"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-url-scheme-allowed-list',
};
/** @type {import('stylelint').Rule<string | RegExp | Array<string | RegExp>>} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [isString, isRegExp],
});
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
functionArgumentsSearch(decl.toString().toLowerCase(), 'url', (args, index) => {
const unspacedUrlString = args.trim();
if (!isStandardSyntaxUrl(unspacedUrlString)) {
return;
}
const urlString = unspacedUrlString.replace(/^['"]+|['"]+$/g, '');
const scheme = getSchemeFromUrl(urlString);
if (scheme === null) {
return;
}
if (matchesStringOrRegExp(scheme, primary)) {
return;
}
report({
message: messages.rejected(scheme),
node: decl,
index,
endIndex: index + args.length,
result,
ruleName,
});
});
});
};
};
rule.primaryOptionArray = true;
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,71 @@
'use strict';
const functionArgumentsSearch = require('../../utils/functionArgumentsSearch');
const getSchemeFromUrl = require('../../utils/getSchemeFromUrl');
const isStandardSyntaxUrl = require('../../utils/isStandardSyntaxUrl');
const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'function-url-scheme-disallowed-list';
const messages = ruleMessages(ruleName, {
rejected: (scheme) => `Unexpected URL scheme "${scheme}:"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-url-scheme-disallowed-list',
};
/** @type {import('stylelint').Rule<string | RegExp | Array<string | RegExp>>} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [isString, isRegExp],
});
if (!validOptions) {
return;
}
root.walkDecls((decl) => {
functionArgumentsSearch(decl.toString().toLowerCase(), 'url', (args, index) => {
const unspacedUrlString = args.trim();
if (!isStandardSyntaxUrl(unspacedUrlString)) {
return;
}
const urlString = unspacedUrlString.replace(/^['"]+|['"]+$/g, '');
const scheme = getSchemeFromUrl(urlString);
if (scheme === null) {
return;
}
if (!matchesStringOrRegExp(scheme, primary)) {
return;
}
report({
message: messages.rejected(scheme),
node: decl,
index,
endIndex: index + args.length,
result,
ruleName,
});
});
});
};
};
rule.primaryOptionArray = true;
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,193 @@
'use strict';
const atRuleParamIndex = require('../../utils/atRuleParamIndex');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const isWhitespace = require('../../utils/isWhitespace');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const setDeclarationValue = require('../../utils/setDeclarationValue');
const styleSearch = require('style-search');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'function-whitespace-after';
const messages = ruleMessages(ruleName, {
expected: 'Expected whitespace after ")"',
rejected: 'Unexpected whitespace after ")"',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/function-whitespace-after',
fixable: true,
};
const ACCEPTABLE_AFTER_CLOSING_PAREN = new Set([')', ',', '}', ':', '/', undefined]);
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['always', 'never'],
});
if (!validOptions) {
return;
}
/**
* @param {import('postcss').Node} node
* @param {string} value
* @param {number} nodeIndex
* @param {((index: number) => void) | undefined} fix
*/
function check(node, value, nodeIndex, fix) {
styleSearch(
{
source: value,
target: ')',
functionArguments: 'only',
},
(match) => {
checkClosingParen(value, match.startIndex + 1, node, nodeIndex, fix);
},
);
}
/**
* @param {string} source
* @param {number} index
* @param {import('postcss').Node} node
* @param {number} nodeIndex
* @param {((index: number) => void) | undefined} fix
*/
function checkClosingParen(source, index, node, nodeIndex, fix) {
const nextChar = source.charAt(index);
if (!nextChar) return;
if (primary === 'always') {
// Allow for the next character to be a single empty space,
// another closing parenthesis, a comma, or the end of the value
if (nextChar === ' ') {
return;
}
if (nextChar === '\n') {
return;
}
if (source.slice(index, index + 2) === '\r\n') {
return;
}
if (ACCEPTABLE_AFTER_CLOSING_PAREN.has(nextChar)) {
return;
}
if (fix) {
fix(index);
return;
}
report({
message: messages.expected,
node,
index: nodeIndex + index,
result,
ruleName,
});
} else if (primary === 'never' && isWhitespace(nextChar)) {
if (fix) {
fix(index);
return;
}
report({
message: messages.rejected,
node,
index: nodeIndex + index,
result,
ruleName,
});
}
}
/**
* @param {string} value
*/
function createFixer(value) {
let fixed = '';
let lastIndex = 0;
/** @type {(index: number) => void} */
let applyFix;
if (primary === 'always') {
applyFix = (index) => {
// eslint-disable-next-line prefer-template
fixed += value.slice(lastIndex, index) + ' ';
lastIndex = index;
};
} else if (primary === 'never') {
applyFix = (index) => {
let whitespaceEndIndex = index + 1;
while (
whitespaceEndIndex < value.length &&
isWhitespace(value.charAt(whitespaceEndIndex))
) {
whitespaceEndIndex++;
}
fixed += value.slice(lastIndex, index);
lastIndex = whitespaceEndIndex;
};
} else {
throw new Error(`Unexpected option: "${primary}"`);
}
return {
applyFix,
get hasFixed() {
return Boolean(lastIndex);
},
get fixed() {
return fixed + value.slice(lastIndex);
},
};
}
root.walkAtRules(/^import$/i, (atRule) => {
const param = (atRule.raws.params && atRule.raws.params.raw) || atRule.params;
const fixer = context.fix && createFixer(param);
check(atRule, param, atRuleParamIndex(atRule), fixer ? fixer.applyFix : undefined);
if (fixer && fixer.hasFixed) {
if (atRule.raws.params) {
atRule.raws.params.raw = fixer.fixed;
} else {
atRule.params = fixer.fixed;
}
}
});
root.walkDecls((decl) => {
const value = getDeclarationValue(decl);
const fixer = context.fix && createFixer(value);
check(decl, value, declarationValueIndex(decl), fixer ? fixer.applyFix : undefined);
if (fixer && fixer.hasFixed) {
setDeclarationValue(decl, fixer.fixed);
}
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,123 @@
'use strict';
const declarationValueIndex = require('../utils/declarationValueIndex');
const getDeclarationValue = require('../utils/getDeclarationValue');
const isStandardSyntaxFunction = require('../utils/isStandardSyntaxFunction');
const report = require('../utils/report');
const setDeclarationValue = require('../utils/setDeclarationValue');
const valueParser = require('postcss-value-parser');
/** @typedef {import('postcss-value-parser').Node} ValueParserNode */
/** @typedef {import('postcss-value-parser').DivNode} ValueParserDivNode */
/** @typedef {(args: { source: string, index: number, err: (message: string) => void }) => void} LocationChecker */
/**
* @param {{
* root: import('postcss').Root,
* locationChecker: LocationChecker,
* fix: ((node: ValueParserDivNode, index: number, nodes: ValueParserNode[]) => boolean) | null,
* result: import('stylelint').PostcssResult,
* checkedRuleName: string,
* }} opts
*/
module.exports = function functionCommaSpaceChecker(opts) {
opts.root.walkDecls((decl) => {
const declValue = getDeclarationValue(decl);
let hasFixed;
const parsedValue = valueParser(declValue);
parsedValue.walk((valueNode) => {
if (valueNode.type !== 'function') {
return;
}
if (!isStandardSyntaxFunction(valueNode)) {
return;
}
// Ignore `url()` arguments, which may contain data URIs or other funky stuff
if (valueNode.value.toLowerCase() === 'url') {
return;
}
const argumentStrings = valueNode.nodes.map((node) => valueParser.stringify(node));
const functionArguments = (() => {
// Remove function name and parens
let result = valueNode.before + argumentStrings.join('') + valueNode.after;
// 1. Remove comments including preceding whitespace (when only succeeded by whitespace)
// 2. Remove all other comments, but leave adjacent whitespace intact
// eslint-disable-next-line regexp/no-dupe-disjunctions -- TODO: Possible to simplify the regex.
result = result.replace(/( *\/(\*.*\*\/(?!\S)|\/.*)|(\/(\*.*\*\/|\/.*)))/, '');
return result;
})();
/**
* Gets the index of the comma for checking.
* @param {ValueParserDivNode} commaNode The comma node
* @param {number} nodeIndex The index of the comma node
* @returns {number} The index of the comma for checking
*/
const getCommaCheckIndex = (commaNode, nodeIndex) => {
let commaBefore =
valueNode.before + argumentStrings.slice(0, nodeIndex).join('') + commaNode.before;
// 1. Remove comments including preceding whitespace (when only succeeded by whitespace)
// 2. Remove all other comments, but leave adjacent whitespace intact
// eslint-disable-next-line regexp/no-dupe-disjunctions -- TODO: Possible to simplify the regex.
commaBefore = commaBefore.replace(/( *\/(\*.*\*\/(?!\S)|\/.*)|(\/(\*.*\*\/|\/.*)))/, '');
return commaBefore.length;
};
/** @type {{ commaNode: ValueParserDivNode, checkIndex: number, nodeIndex: number }[]} */
const commaDataList = [];
for (const [nodeIndex, node] of valueNode.nodes.entries()) {
if (node.type !== 'div' || node.value !== ',') {
continue;
}
const checkIndex = getCommaCheckIndex(node, nodeIndex);
commaDataList.push({
commaNode: node,
checkIndex,
nodeIndex,
});
}
for (const { commaNode, checkIndex, nodeIndex } of commaDataList) {
opts.locationChecker({
source: functionArguments,
index: checkIndex,
err: (message) => {
const index =
declarationValueIndex(decl) + commaNode.sourceIndex + commaNode.before.length;
if (opts.fix && opts.fix(commaNode, nodeIndex, valueNode.nodes)) {
hasFixed = true;
return;
}
report({
index,
message,
node: decl,
result: opts.result,
ruleName: opts.checkedRuleName,
});
},
});
}
});
if (hasFixed) {
setDeclarationValue(decl, parsedValue.toString());
}
});
};

View File

@@ -0,0 +1,49 @@
'use strict';
/**
* @param {{
* div: import('postcss-value-parser').DivNode,
* index: number,
* nodes: import('postcss-value-parser').Node[],
* expectation: string,
* position: 'before' | 'after',
* symb: string,
* }} params
* @returns {boolean}
*/
module.exports = function functionCommaSpaceFix(params) {
const { div, index, nodes, expectation, position, symb } = params;
if (expectation.startsWith('always')) {
div[position] = symb;
return true;
}
if (expectation.startsWith('never')) {
div[position] = '';
for (let i = index + 1; i < nodes.length; i++) {
const node = nodes[i];
if (node === undefined) {
continue;
}
if (node.type === 'comment') {
continue;
}
if (node.type === 'space') {
node.value = '';
continue;
}
break;
}
return true;
}
return false;
};

View File

@@ -0,0 +1,147 @@
'use strict';
const valueParser = require('postcss-value-parser');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const setDeclarationValue = require('../../utils/setDeclarationValue');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'hue-degree-notation';
const messages = ruleMessages(ruleName, {
expected: (unfixed, fixed) => `Expected "${unfixed}" to be "${fixed}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/hue-degree-notation',
fixable: true,
};
const HUE_FIRST_ARG_FUNCS = ['hsl', 'hsla', 'hwb'];
const HUE_THIRD_ARG_FUNCS = ['lch'];
const HUE_FUNCS = new Set([...HUE_FIRST_ARG_FUNCS, ...HUE_THIRD_ARG_FUNCS]);
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['angle', 'number'],
});
if (!validOptions) return;
root.walkDecls((decl) => {
let needsFix = false;
const parsedValue = valueParser(getDeclarationValue(decl));
parsedValue.walk((node) => {
if (node.type !== 'function') return;
if (!HUE_FUNCS.has(node.value.toLowerCase())) return;
const hue = findHue(node);
if (!hue) return;
const { value } = hue;
if (!isStandardSyntaxValue(value)) return;
if (!isDegree(value) && !isNumber(value)) return;
if (primary === 'angle' && isDegree(value)) return;
if (primary === 'number' && isNumber(value)) return;
const fixed = primary === 'angle' ? asDegree(value) : asNumber(value);
const unfixed = value;
if (context.fix) {
hue.value = fixed;
needsFix = true;
return;
}
const valueIndex = declarationValueIndex(decl);
report({
message: messages.expected(unfixed, fixed),
node: decl,
index: valueIndex + hue.sourceIndex,
endIndex: valueIndex + hue.sourceEndIndex,
result,
ruleName,
});
});
if (needsFix) {
setDeclarationValue(decl, parsedValue.toString());
}
});
};
};
/**
* @param {string} value
*/
function asDegree(value) {
return `${value}deg`;
}
/**
* @param {string} value
*/
function asNumber(value) {
const dimension = valueParser.unit(value);
if (dimension) return dimension.number;
throw new TypeError(`The "${value}" value must have a unit`);
}
/**
* @param {import('postcss-value-parser').FunctionNode} node
*/
function findHue(node) {
const args = node.nodes.filter(({ type }) => type === 'word' || type === 'function');
const value = node.value.toLowerCase();
if (HUE_FIRST_ARG_FUNCS.includes(value)) {
return args[0];
}
if (HUE_THIRD_ARG_FUNCS.includes(value)) {
return args[2];
}
return undefined;
}
/**
* @param {string} value
*/
function isDegree(value) {
const dimension = valueParser.unit(value);
return dimension && dimension.unit.toLowerCase() === 'deg';
}
/**
* @param {string} value
*/
function isNumber(value) {
const dimension = valueParser.unit(value);
return dimension && dimension.unit === '';
}
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,126 @@
'use strict';
const valueParser = require('postcss-value-parser');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const setAtRuleParams = require('../../utils/setAtRuleParams');
const getAtRuleParams = require('../../utils/getAtRuleParams');
const atRuleParamIndex = require('../../utils/atRuleParamIndex');
const ruleName = 'import-notation';
const messages = ruleMessages(ruleName, {
expected: (unfixed, fixed) => `Expected "${unfixed}" to be "${fixed}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/import-notation',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['string', 'url'],
});
if (!validOptions) {
return;
}
root.walkAtRules(/^import$/i, checkAtRuleImportParams);
/**
* @param {import('postcss').AtRule} atRule
*/
function checkAtRuleImportParams(atRule) {
const params = getAtRuleParams(atRule);
const parsed = valueParser(params);
for (const node of parsed.nodes) {
const start = atRuleParamIndex(atRule);
const end = start + node.sourceEndIndex;
if (primary === 'string') {
if (node.type === 'function' && node.value.toLowerCase() === 'url') {
const urlFunctionFull = valueParser.stringify(node);
const urlFunctionArguments = valueParser.stringify(node.nodes);
const quotedUrlFunctionFirstArgument =
node.nodes[0] && node.nodes[0].type === 'word'
? `"${urlFunctionArguments}"`
: urlFunctionArguments;
if (context.fix) {
const restAtRuleParams = atRule.params.slice(node.sourceEndIndex);
setAtRuleParams(atRule, `${quotedUrlFunctionFirstArgument}${restAtRuleParams}`);
return;
}
complain(
messages.expected(urlFunctionFull, quotedUrlFunctionFirstArgument),
atRule,
start,
end,
);
return;
}
}
if (primary === 'url') {
if (node.type === 'space') return;
if (node.type === 'word' || node.type === 'string') {
const path = valueParser.stringify(node);
const urlFunctionFull = `url(${path})`;
if (context.fix) {
const restAtRuleParams = atRule.params.slice(node.sourceEndIndex);
setAtRuleParams(atRule, `${urlFunctionFull}${restAtRuleParams}`);
return;
}
const quotedNodeValue =
node.type === 'word' ? `"${node.value}"` : `${node.quote}${node.value}${node.quote}`;
complain(messages.expected(quotedNodeValue, urlFunctionFull), atRule, start, end);
return;
}
}
}
}
/**
* @param {string} message
* @param {import('postcss').Node} node
* @param {number} index
* @param {number} endIndex
*/
function complain(message, node, index, endIndex) {
report({
message,
node,
index,
endIndex,
result,
ruleName,
});
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

732
node_modules/stylelint/lib/rules/indentation/index.js generated vendored Normal file
View File

@@ -0,0 +1,732 @@
'use strict';
const beforeBlockString = require('../../utils/beforeBlockString');
const hasBlock = require('../../utils/hasBlock');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const styleSearch = require('style-search');
const validateOptions = require('../../utils/validateOptions');
const { isAtRule, isDeclaration, isRoot, isRule } = require('../../utils/typeGuards');
const { isBoolean, isNumber, isString, assertString } = require('../../utils/validateTypes');
const ruleName = 'indentation';
const messages = ruleMessages(ruleName, {
expected: (x) => `Expected indentation of ${x}`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/indentation',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions = {}, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
possible: [isNumber, 'tab'],
},
{
actual: secondaryOptions,
possible: {
baseIndentLevel: [isNumber, 'auto'],
except: ['block', 'value', 'param'],
ignore: ['value', 'param', 'inside-parens'],
indentInsideParens: ['twice', 'once-at-root-twice-in-block'],
indentClosingBrace: [isBoolean],
},
optional: true,
},
);
if (!validOptions) {
return;
}
const spaceCount = isNumber(primary) ? primary : null;
const indentChar = spaceCount == null ? '\t' : ' '.repeat(spaceCount);
const warningWord = primary === 'tab' ? 'tab' : 'space';
/** @type {number | 'auto'} */
const baseIndentLevel = secondaryOptions.baseIndentLevel;
/** @type {boolean} */
const indentClosingBrace = secondaryOptions.indentClosingBrace;
/**
* @param {number} level
*/
const legibleExpectation = (level) => {
const count = spaceCount == null ? level : level * spaceCount;
const quantifiedWarningWord = count === 1 ? warningWord : `${warningWord}s`;
return `${count} ${quantifiedWarningWord}`;
};
// Cycle through all nodes using walk.
root.walk((node) => {
if (isRoot(node)) {
// Ignore nested template literals root in css-in-js lang
return;
}
const nodeLevel = indentationLevel(node);
// Cut out any * and _ hacks from `before`
const before = (node.raws.before || '').replace(/[*_]$/, '');
const after = typeof node.raws.after === 'string' ? node.raws.after : '';
const parent = node.parent;
if (!parent) throw new Error('A parent node must be present');
const expectedOpeningBraceIndentation = indentChar.repeat(nodeLevel);
// Only inspect the spaces before the node
// if this is the first node in root
// or there is a newline in the `before` string.
// (If there is no newline before a node,
// there is no "indentation" to check.)
const isFirstChild = parent.type === 'root' && parent.first === node;
const lastIndexOfNewline = before.lastIndexOf('\n');
// Inspect whitespace in the `before` string that is
// *after* the *last* newline character,
// because anything besides that is not indentation for this node:
// it is some other kind of separation, checked by some separate rule
if (
(lastIndexOfNewline !== -1 ||
(isFirstChild &&
(!getDocument(parent) ||
(parent.raws.codeBefore && parent.raws.codeBefore.endsWith('\n'))))) &&
before.slice(lastIndexOfNewline + 1) !== expectedOpeningBraceIndentation
) {
if (context.fix) {
if (isFirstChild && isString(node.raws.before)) {
node.raws.before = node.raws.before.replace(
/^[ \t]*(?=\S|$)/,
expectedOpeningBraceIndentation,
);
}
node.raws.before = fixIndentation(node.raws.before, expectedOpeningBraceIndentation);
} else {
report({
message: messages.expected(legibleExpectation(nodeLevel)),
node,
result,
ruleName,
});
}
}
// Only blocks have the `after` string to check.
// Only inspect `after` strings that start with a newline;
// otherwise there's no indentation involved.
// And check `indentClosingBrace` to see if it should be indented an extra level.
const closingBraceLevel = indentClosingBrace ? nodeLevel + 1 : nodeLevel;
const expectedClosingBraceIndentation = indentChar.repeat(closingBraceLevel);
if (
(isRule(node) || isAtRule(node)) &&
hasBlock(node) &&
after &&
after.includes('\n') &&
after.slice(after.lastIndexOf('\n') + 1) !== expectedClosingBraceIndentation
) {
if (context.fix) {
node.raws.after = fixIndentation(node.raws.after, expectedClosingBraceIndentation);
} else {
report({
message: messages.expected(legibleExpectation(closingBraceLevel)),
node,
index: node.toString().length - 1,
result,
ruleName,
});
}
}
// If this is a declaration, check the value
if (isDeclaration(node)) {
checkValue(node, nodeLevel);
}
// If this is a rule, check the selector
if (isRule(node)) {
checkSelector(node, nodeLevel);
}
// If this is an at rule, check the params
if (isAtRule(node)) {
checkAtRuleParams(node, nodeLevel);
}
});
/**
* @param {import('postcss').Node} node
* @param {number} level
* @returns {number}
*/
function indentationLevel(node, level = 0) {
if (!node.parent) throw new Error('A parent node must be present');
if (isRoot(node.parent)) {
return level + getRootBaseIndentLevel(node.parent, baseIndentLevel, primary);
}
let calculatedLevel;
// Indentation level equals the ancestor nodes
// separating this node from root; so recursively
// run this operation
calculatedLevel = indentationLevel(node.parent, level + 1);
// If `secondaryOptions.except` includes "block",
// blocks are taken down one from their calculated level
// (all blocks are the same level as their parents)
if (
optionsMatches(secondaryOptions, 'except', 'block') &&
(isRule(node) || isAtRule(node)) &&
hasBlock(node)
) {
calculatedLevel--;
}
return calculatedLevel;
}
/**
* @param {import('postcss').Declaration} decl
* @param {number} declLevel
*/
function checkValue(decl, declLevel) {
if (!decl.value.includes('\n')) {
return;
}
if (optionsMatches(secondaryOptions, 'ignore', 'value')) {
return;
}
const declString = decl.toString();
const valueLevel = optionsMatches(secondaryOptions, 'except', 'value')
? declLevel
: declLevel + 1;
checkMultilineBit(declString, valueLevel, decl);
}
/**
* @param {import('postcss').Rule} ruleNode
* @param {number} ruleLevel
*/
function checkSelector(ruleNode, ruleLevel) {
const selector = ruleNode.selector;
// Less mixins have params, and they should be indented extra
// @ts-expect-error -- TS2339: Property 'params' does not exist on type 'Rule'.
if (ruleNode.params) {
ruleLevel += 1;
}
checkMultilineBit(selector, ruleLevel, ruleNode);
}
/**
* @param {import('postcss').AtRule} atRule
* @param {number} ruleLevel
*/
function checkAtRuleParams(atRule, ruleLevel) {
if (optionsMatches(secondaryOptions, 'ignore', 'param')) {
return;
}
// @nest and SCSS's @at-root rules should be treated like regular rules, not expected
// to have their params (selectors) indented
const paramLevel =
optionsMatches(secondaryOptions, 'except', 'param') ||
atRule.name === 'nest' ||
atRule.name === 'at-root'
? ruleLevel
: ruleLevel + 1;
checkMultilineBit(beforeBlockString(atRule).trim(), paramLevel, atRule);
}
/**
* @param {string} source
* @param {number} newlineIndentLevel
* @param {import('postcss').Node} node
*/
function checkMultilineBit(source, newlineIndentLevel, node) {
if (!source.includes('\n')) {
return;
}
// Data for current node fixing
/** @type {Array<{ expectedIndentation: string, currentIndentation: string, startIndex: number }>} */
const fixPositions = [];
// `outsideParens` because function arguments and also non-standard parenthesized stuff like
// Sass maps are ignored to allow for arbitrary indentation
let parentheticalDepth = 0;
const ignoreInsideParans = optionsMatches(secondaryOptions, 'ignore', 'inside-parens');
styleSearch(
{
source,
target: '\n',
// @ts-expect-error -- The `outsideParens` option is unsupported. Why?
outsideParens: ignoreInsideParans,
},
(match, matchCount) => {
const precedesClosingParenthesis = /^[ \t]*\)/.test(source.slice(match.startIndex + 1));
if (ignoreInsideParans && (precedesClosingParenthesis || match.insideParens)) {
return;
}
let expectedIndentLevel = newlineIndentLevel;
// Modififications for parenthetical content
if (!ignoreInsideParans && match.insideParens) {
// If the first match in is within parentheses, reduce the parenthesis penalty
if (matchCount === 1) parentheticalDepth -= 1;
// Account for windows line endings
let newlineIndex = match.startIndex;
if (source[match.startIndex - 1] === '\r') {
newlineIndex--;
}
const followsOpeningParenthesis = /\([ \t]*$/.test(source.slice(0, newlineIndex));
if (followsOpeningParenthesis) {
parentheticalDepth += 1;
}
const followsOpeningBrace = /\{[ \t]*$/.test(source.slice(0, newlineIndex));
if (followsOpeningBrace) {
parentheticalDepth += 1;
}
const startingClosingBrace = /^[ \t]*\}/.test(source.slice(match.startIndex + 1));
if (startingClosingBrace) {
parentheticalDepth -= 1;
}
expectedIndentLevel += parentheticalDepth;
// Past this point, adjustments to parentheticalDepth affect next line
if (precedesClosingParenthesis) {
parentheticalDepth -= 1;
}
switch (secondaryOptions.indentInsideParens) {
case 'twice':
if (!precedesClosingParenthesis || indentClosingBrace) {
expectedIndentLevel += 1;
}
break;
case 'once-at-root-twice-in-block':
if (node.parent === node.root()) {
if (precedesClosingParenthesis && !indentClosingBrace) {
expectedIndentLevel -= 1;
}
break;
}
if (!precedesClosingParenthesis || indentClosingBrace) {
expectedIndentLevel += 1;
}
break;
default:
if (precedesClosingParenthesis && !indentClosingBrace) {
expectedIndentLevel -= 1;
}
}
}
// Starting at the index after the newline, we want to
// check that the whitespace characters (excluding newlines) before the first
// non-whitespace character equal the expected indentation
const afterNewlineSpaceMatches = /^([ \t]*)\S/.exec(source.slice(match.startIndex + 1));
if (!afterNewlineSpaceMatches) {
return;
}
const afterNewlineSpace = afterNewlineSpaceMatches[1] || '';
const expectedIndentation = indentChar.repeat(
expectedIndentLevel > 0 ? expectedIndentLevel : 0,
);
if (afterNewlineSpace !== expectedIndentation) {
if (context.fix) {
// Adding fixes position in reverse order, because if we change indent in the beginning of the string it will break all following fixes for that string
fixPositions.unshift({
expectedIndentation,
currentIndentation: afterNewlineSpace,
startIndex: match.startIndex,
});
} else {
report({
message: messages.expected(legibleExpectation(expectedIndentLevel)),
node,
index: match.startIndex + afterNewlineSpace.length + 1,
result,
ruleName,
});
}
}
},
);
if (fixPositions.length) {
if (isRule(node)) {
for (const fixPosition of fixPositions) {
node.selector = replaceIndentation(
node.selector,
fixPosition.currentIndentation,
fixPosition.expectedIndentation,
fixPosition.startIndex,
);
}
}
if (isDeclaration(node)) {
const declProp = node.prop;
const declBetween = node.raws.between;
if (!isString(declBetween)) {
throw new TypeError('The `between` property must be a string');
}
for (const fixPosition of fixPositions) {
if (fixPosition.startIndex < declProp.length + declBetween.length) {
node.raws.between = replaceIndentation(
declBetween,
fixPosition.currentIndentation,
fixPosition.expectedIndentation,
fixPosition.startIndex - declProp.length,
);
} else {
node.value = replaceIndentation(
node.value,
fixPosition.currentIndentation,
fixPosition.expectedIndentation,
fixPosition.startIndex - declProp.length - declBetween.length,
);
}
}
}
if (isAtRule(node)) {
const atRuleName = node.name;
const atRuleAfterName = node.raws.afterName;
const atRuleParams = node.params;
if (!isString(atRuleAfterName)) {
throw new TypeError('The `afterName` property must be a string');
}
for (const fixPosition of fixPositions) {
// 1 — it's a @ length
if (fixPosition.startIndex < 1 + atRuleName.length + atRuleAfterName.length) {
node.raws.afterName = replaceIndentation(
atRuleAfterName,
fixPosition.currentIndentation,
fixPosition.expectedIndentation,
fixPosition.startIndex - atRuleName.length - 1,
);
} else {
node.params = replaceIndentation(
atRuleParams,
fixPosition.currentIndentation,
fixPosition.expectedIndentation,
fixPosition.startIndex - atRuleName.length - atRuleAfterName.length - 1,
);
}
}
}
}
}
};
};
/**
* @param {import('postcss').Root} root
* @param {number | 'auto'} baseIndentLevel
* @param {string} space
* @returns {number}
*/
function getRootBaseIndentLevel(root, baseIndentLevel, space) {
const document = getDocument(root);
if (!document) {
return 0;
}
if (!root.source) {
throw new Error('The root node must have a source');
}
/** @type {import('postcss').Source & { baseIndentLevel?: number }} */
const source = root.source;
const indentLevel = source.baseIndentLevel;
if (isNumber(indentLevel) && Number.isSafeInteger(indentLevel)) {
return indentLevel;
}
const newIndentLevel = inferRootIndentLevel(root, baseIndentLevel, () =>
inferDocIndentSize(document, space),
);
source.baseIndentLevel = newIndentLevel;
return newIndentLevel;
}
/**
* @param {import('postcss').Node} node
*/
function getDocument(node) {
// @ts-expect-error -- TS2339: Property 'document' does not exist on type 'Node'.
const document = node.document;
if (document) {
return document;
}
const root = node.root();
// @ts-expect-error -- TS2339: Property 'document' does not exist on type 'Node'.
return root && root.document;
}
/**
* @param {import('postcss').Document} document
* @param {string} space
* returns {number}
*/
function inferDocIndentSize(document, space) {
if (!document.source) throw new Error('The document node must have a source');
/** @type {import('postcss').Source & { indentSize?: number }} */
const docSource = document.source;
let indentSize = docSource.indentSize;
if (isNumber(indentSize) && Number.isSafeInteger(indentSize)) {
return indentSize;
}
const source = document.source.input.css;
const indents = source.match(/^ *(?=\S)/gm);
if (indents) {
/** @type {Map<number, number>} */
const scores = new Map();
let lastIndentSize = 0;
let lastLeadingSpacesLength = 0;
/**
* @param {number} leadingSpacesLength
*/
const vote = (leadingSpacesLength) => {
if (leadingSpacesLength) {
lastIndentSize = Math.abs(leadingSpacesLength - lastLeadingSpacesLength) || lastIndentSize;
if (lastIndentSize > 1) {
const score = scores.get(lastIndentSize);
if (score) {
scores.set(lastIndentSize, score + 1);
} else {
scores.set(lastIndentSize, 1);
}
}
} else {
lastIndentSize = 0;
}
lastLeadingSpacesLength = leadingSpacesLength;
};
for (const leadingSpaces of indents) {
vote(leadingSpaces.length);
}
let bestScore = 0;
for (const [indentSizeDate, score] of scores.entries()) {
if (score > bestScore) {
bestScore = score;
indentSize = indentSizeDate;
}
}
}
indentSize =
Number(indentSize) || (indents && indents[0] && indents[0].length) || Number(space) || 2;
docSource.indentSize = indentSize;
return indentSize;
}
/**
* @param {import('postcss').Root} root
* @param {number | 'auto'} baseIndentLevel
* @param {() => number} indentSize
* @returns {number}
*/
function inferRootIndentLevel(root, baseIndentLevel, indentSize) {
/**
* @param {string} indent
*/
function getIndentLevel(indent) {
const tabMatch = indent.match(/\t/g);
const tabCount = tabMatch ? tabMatch.length : 0;
const spaceMatch = indent.match(/ /g);
const spaceCount = spaceMatch ? Math.round(spaceMatch.length / indentSize()) : 0;
return tabCount + spaceCount;
}
let newBaseIndentLevel = 0;
if (!isNumber(baseIndentLevel) || !Number.isSafeInteger(baseIndentLevel)) {
if (!root.source) throw new Error('The root node must have a source');
let source = root.source.input.css;
source = source.replace(/^[^\r\n]+/, (firstLine) => {
const match = root.raws.codeBefore && /(?:^|\n)([ \t]*)$/.exec(root.raws.codeBefore);
if (match) {
return match[1] + firstLine;
}
return '';
});
const indents = source.match(/^[ \t]*(?=\S)/gm);
if (indents) {
return Math.min(...indents.map((indent) => getIndentLevel(indent)));
}
newBaseIndentLevel = 1;
} else {
newBaseIndentLevel = baseIndentLevel;
}
const indents = [];
const foundIndents = root.raws.codeBefore && /(?:^|\n)([ \t]*)\S/m.exec(root.raws.codeBefore);
// The indent level of the CSS code block in non-CSS-like files is determined by the shortest indent of non-empty line.
if (foundIndents) {
let shortest = Number.MAX_SAFE_INTEGER;
let i = 0;
while (++i < foundIndents.length) {
const foundIndent = foundIndents[i];
assertString(foundIndent);
const current = getIndentLevel(foundIndent);
if (current < shortest) {
shortest = current;
if (shortest === 0) {
break;
}
}
}
if (shortest !== Number.MAX_SAFE_INTEGER) {
indents.push(new Array(shortest).fill(' ').join(''));
}
}
const after = root.raws.after;
if (after) {
let afterEnd;
if (after.endsWith('\n')) {
// @ts-expect-error -- TS2339: Property 'document' does not exist on type 'Root'.
const document = root.document;
if (document) {
const nextRoot = document.nodes[document.nodes.indexOf(root) + 1];
afterEnd = nextRoot ? nextRoot.raws.codeBefore : document.raws.codeAfter;
} else {
// Nested root node in css-in-js lang
const parent = root.parent;
if (!parent) throw new Error('The root node must have a parent');
const nextRoot = parent.nodes[parent.nodes.indexOf(root) + 1];
afterEnd = nextRoot ? nextRoot.raws.codeBefore : root.raws.codeAfter;
}
} else {
afterEnd = after;
}
if (afterEnd) indents.push(afterEnd.match(/^[ \t]*/)[0]);
}
if (indents.length) {
return Math.max(...indents.map((indent) => getIndentLevel(indent))) + newBaseIndentLevel;
}
return newBaseIndentLevel;
}
/**
* @param {string | undefined} str
* @param {string} whitespace
*/
function fixIndentation(str, whitespace) {
if (!isString(str)) {
return str;
}
return str.replace(/\n[ \t]*(?=\S|$)/g, `\n${whitespace}`);
}
/**
* @param {string} input
* @param {string} searchString
* @param {string} replaceString
* @param {number} startIndex
*/
function replaceIndentation(input, searchString, replaceString, startIndex) {
const offset = startIndex + 1;
const stringStart = input.slice(0, offset);
const stringEnd = input.slice(offset + searchString.length);
return stringStart + replaceString + stringEnd;
}
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

366
node_modules/stylelint/lib/rules/index.js generated vendored Normal file
View File

@@ -0,0 +1,366 @@
'use strict';
const importLazy = require('import-lazy');
/** @type {typeof import('stylelint').rules} */
const rules = {
'alpha-value-notation': importLazy(() => require('./alpha-value-notation'))(),
'annotation-no-unknown': importLazy(() => require('./annotation-no-unknown'))(),
'at-rule-allowed-list': importLazy(() => require('./at-rule-allowed-list'))(),
'at-rule-disallowed-list': importLazy(() => require('./at-rule-disallowed-list'))(),
'at-rule-empty-line-before': importLazy(() => require('./at-rule-empty-line-before'))(),
'at-rule-name-case': importLazy(() => require('./at-rule-name-case'))(),
'at-rule-name-newline-after': importLazy(() => require('./at-rule-name-newline-after'))(),
'at-rule-semicolon-space-before': importLazy(() => require('./at-rule-semicolon-space-before'))(),
'at-rule-name-space-after': importLazy(() => require('./at-rule-name-space-after'))(),
'at-rule-no-unknown': importLazy(() => require('./at-rule-no-unknown'))(),
'at-rule-no-vendor-prefix': importLazy(() => require('./at-rule-no-vendor-prefix'))(),
'at-rule-property-required-list': importLazy(() => require('./at-rule-property-required-list'))(),
'at-rule-semicolon-newline-after': importLazy(() =>
require('./at-rule-semicolon-newline-after'),
)(),
'block-closing-brace-empty-line-before': importLazy(() =>
require('./block-closing-brace-empty-line-before'),
)(),
'block-closing-brace-newline-after': importLazy(() =>
require('./block-closing-brace-newline-after'),
)(),
'block-closing-brace-newline-before': importLazy(() =>
require('./block-closing-brace-newline-before'),
)(),
'block-closing-brace-space-after': importLazy(() =>
require('./block-closing-brace-space-after'),
)(),
'block-closing-brace-space-before': importLazy(() =>
require('./block-closing-brace-space-before'),
)(),
'block-no-empty': importLazy(() => require('./block-no-empty'))(),
'block-opening-brace-newline-after': importLazy(() =>
require('./block-opening-brace-newline-after'),
)(),
'block-opening-brace-newline-before': importLazy(() =>
require('./block-opening-brace-newline-before'),
)(),
'block-opening-brace-space-after': importLazy(() =>
require('./block-opening-brace-space-after'),
)(),
'block-opening-brace-space-before': importLazy(() =>
require('./block-opening-brace-space-before'),
)(),
'color-function-notation': importLazy(() => require('./color-function-notation'))(),
'color-hex-alpha': importLazy(() => require('./color-hex-alpha'))(),
'color-hex-case': importLazy(() => require('./color-hex-case'))(),
'color-hex-length': importLazy(() => require('./color-hex-length'))(),
'color-named': importLazy(() => require('./color-named'))(),
'color-no-hex': importLazy(() => require('./color-no-hex'))(),
'color-no-invalid-hex': importLazy(() => require('./color-no-invalid-hex'))(),
'comment-empty-line-before': importLazy(() => require('./comment-empty-line-before'))(),
'comment-no-empty': importLazy(() => require('./comment-no-empty'))(),
'comment-pattern': importLazy(() => require('./comment-pattern'))(),
'comment-whitespace-inside': importLazy(() => require('./comment-whitespace-inside'))(),
'comment-word-disallowed-list': importLazy(() => require('./comment-word-disallowed-list'))(),
'custom-media-pattern': importLazy(() => require('./custom-media-pattern'))(),
'custom-property-empty-line-before': importLazy(() =>
require('./custom-property-empty-line-before'),
)(),
'custom-property-no-missing-var-function': importLazy(() =>
require('./custom-property-no-missing-var-function'),
)(),
'custom-property-pattern': importLazy(() => require('./custom-property-pattern'))(),
'declaration-bang-space-after': importLazy(() => require('./declaration-bang-space-after'))(),
'declaration-bang-space-before': importLazy(() => require('./declaration-bang-space-before'))(),
'declaration-block-no-duplicate-custom-properties': importLazy(() =>
require('./declaration-block-no-duplicate-custom-properties'),
)(),
'declaration-block-no-duplicate-properties': importLazy(() =>
require('./declaration-block-no-duplicate-properties'),
)(),
'declaration-block-no-redundant-longhand-properties': importLazy(() =>
require('./declaration-block-no-redundant-longhand-properties'),
)(),
'declaration-block-no-shorthand-property-overrides': importLazy(() =>
require('./declaration-block-no-shorthand-property-overrides'),
)(),
'declaration-block-semicolon-newline-after': importLazy(() =>
require('./declaration-block-semicolon-newline-after'),
)(),
'declaration-block-semicolon-newline-before': importLazy(() =>
require('./declaration-block-semicolon-newline-before'),
)(),
'declaration-block-semicolon-space-after': importLazy(() =>
require('./declaration-block-semicolon-space-after'),
)(),
'declaration-block-semicolon-space-before': importLazy(() =>
require('./declaration-block-semicolon-space-before'),
)(),
'declaration-block-single-line-max-declarations': importLazy(() =>
require('./declaration-block-single-line-max-declarations'),
)(),
'declaration-block-trailing-semicolon': importLazy(() =>
require('./declaration-block-trailing-semicolon'),
)(),
'declaration-colon-newline-after': importLazy(() =>
require('./declaration-colon-newline-after'),
)(),
'declaration-colon-space-after': importLazy(() => require('./declaration-colon-space-after'))(),
'declaration-colon-space-before': importLazy(() => require('./declaration-colon-space-before'))(),
'declaration-empty-line-before': importLazy(() => require('./declaration-empty-line-before'))(),
'declaration-no-important': importLazy(() => require('./declaration-no-important'))(),
'declaration-property-max-values': importLazy(() =>
require('./declaration-property-max-values'),
)(),
'declaration-property-unit-allowed-list': importLazy(() =>
require('./declaration-property-unit-allowed-list'),
)(),
'declaration-property-unit-disallowed-list': importLazy(() =>
require('./declaration-property-unit-disallowed-list'),
)(),
'declaration-property-value-allowed-list': importLazy(() =>
require('./declaration-property-value-allowed-list'),
)(),
'declaration-property-value-disallowed-list': importLazy(() =>
require('./declaration-property-value-disallowed-list'),
)(),
'font-family-no-missing-generic-family-keyword': importLazy(() =>
require('./font-family-no-missing-generic-family-keyword'),
)(),
'font-family-name-quotes': importLazy(() => require('./font-family-name-quotes'))(),
'font-family-no-duplicate-names': importLazy(() => require('./font-family-no-duplicate-names'))(),
'font-weight-notation': importLazy(() => require('./font-weight-notation'))(),
'function-allowed-list': importLazy(() => require('./function-allowed-list'))(),
'function-calc-no-unspaced-operator': importLazy(() =>
require('./function-calc-no-unspaced-operator'),
)(),
'function-comma-newline-after': importLazy(() => require('./function-comma-newline-after'))(),
'function-comma-newline-before': importLazy(() => require('./function-comma-newline-before'))(),
'function-comma-space-after': importLazy(() => require('./function-comma-space-after'))(),
'function-comma-space-before': importLazy(() => require('./function-comma-space-before'))(),
'function-disallowed-list': importLazy(() => require('./function-disallowed-list'))(),
'function-linear-gradient-no-nonstandard-direction': importLazy(() =>
require('./function-linear-gradient-no-nonstandard-direction'),
)(),
'function-max-empty-lines': importLazy(() => require('./function-max-empty-lines'))(),
'function-name-case': importLazy(() => require('./function-name-case'))(),
'function-no-unknown': importLazy(() => require('./function-no-unknown'))(),
'function-parentheses-newline-inside': importLazy(() =>
require('./function-parentheses-newline-inside'),
)(),
'function-parentheses-space-inside': importLazy(() =>
require('./function-parentheses-space-inside'),
)(),
'function-url-no-scheme-relative': importLazy(() =>
require('./function-url-no-scheme-relative'),
)(),
'function-url-quotes': importLazy(() => require('./function-url-quotes'))(),
'function-url-scheme-allowed-list': importLazy(() =>
require('./function-url-scheme-allowed-list'),
)(),
'function-url-scheme-disallowed-list': importLazy(() =>
require('./function-url-scheme-disallowed-list'),
)(),
'function-whitespace-after': importLazy(() => require('./function-whitespace-after'))(),
'hue-degree-notation': importLazy(() => require('./hue-degree-notation'))(),
'import-notation': importLazy(() => require('./import-notation'))(),
'keyframe-block-no-duplicate-selectors': importLazy(() =>
require('./keyframe-block-no-duplicate-selectors'),
)(),
'keyframe-declaration-no-important': importLazy(() =>
require('./keyframe-declaration-no-important'),
)(),
'keyframe-selector-notation': importLazy(() => require('./keyframe-selector-notation'))(),
'keyframes-name-pattern': importLazy(() => require('./keyframes-name-pattern'))(),
'length-zero-no-unit': importLazy(() => require('./length-zero-no-unit'))(),
linebreaks: importLazy(() => require('./linebreaks'))(),
'max-empty-lines': importLazy(() => require('./max-empty-lines'))(),
'max-line-length': importLazy(() => require('./max-line-length'))(),
'max-nesting-depth': importLazy(() => require('./max-nesting-depth'))(),
'media-feature-colon-space-after': importLazy(() =>
require('./media-feature-colon-space-after'),
)(),
'media-feature-colon-space-before': importLazy(() =>
require('./media-feature-colon-space-before'),
)(),
'media-feature-name-allowed-list': importLazy(() =>
require('./media-feature-name-allowed-list'),
)(),
'media-feature-name-case': importLazy(() => require('./media-feature-name-case'))(),
'media-feature-name-disallowed-list': importLazy(() =>
require('./media-feature-name-disallowed-list'),
)(),
'media-feature-name-no-unknown': importLazy(() => require('./media-feature-name-no-unknown'))(),
'media-feature-name-no-vendor-prefix': importLazy(() =>
require('./media-feature-name-no-vendor-prefix'),
)(),
'media-feature-name-value-allowed-list': importLazy(() =>
require('./media-feature-name-value-allowed-list'),
)(),
'media-feature-parentheses-space-inside': importLazy(() =>
require('./media-feature-parentheses-space-inside'),
)(),
'media-feature-range-notation': importLazy(() => require('./media-feature-range-notation'))(),
'media-feature-range-operator-space-after': importLazy(() =>
require('./media-feature-range-operator-space-after'),
)(),
'media-feature-range-operator-space-before': importLazy(() =>
require('./media-feature-range-operator-space-before'),
)(),
'media-query-list-comma-newline-after': importLazy(() =>
require('./media-query-list-comma-newline-after'),
)(),
'media-query-list-comma-newline-before': importLazy(() =>
require('./media-query-list-comma-newline-before'),
)(),
'media-query-list-comma-space-after': importLazy(() =>
require('./media-query-list-comma-space-after'),
)(),
'media-query-list-comma-space-before': importLazy(() =>
require('./media-query-list-comma-space-before'),
)(),
'named-grid-areas-no-invalid': importLazy(() => require('./named-grid-areas-no-invalid'))(),
'no-descending-specificity': importLazy(() => require('./no-descending-specificity'))(),
'no-duplicate-at-import-rules': importLazy(() => require('./no-duplicate-at-import-rules'))(),
'no-duplicate-selectors': importLazy(() => require('./no-duplicate-selectors'))(),
'no-empty-source': importLazy(() => require('./no-empty-source'))(),
'no-empty-first-line': importLazy(() => require('./no-empty-first-line'))(),
'no-eol-whitespace': importLazy(() => require('./no-eol-whitespace'))(),
'no-extra-semicolons': importLazy(() => require('./no-extra-semicolons'))(),
'no-invalid-double-slash-comments': importLazy(() =>
require('./no-invalid-double-slash-comments'),
)(),
'no-invalid-position-at-import-rule': importLazy(() =>
require('./no-invalid-position-at-import-rule'),
)(),
'no-irregular-whitespace': importLazy(() => require('./no-irregular-whitespace'))(),
'no-missing-end-of-source-newline': importLazy(() =>
require('./no-missing-end-of-source-newline'),
)(),
'no-unknown-animations': importLazy(() => require('./no-unknown-animations'))(),
'number-leading-zero': importLazy(() => require('./number-leading-zero'))(),
'number-max-precision': importLazy(() => require('./number-max-precision'))(),
'number-no-trailing-zeros': importLazy(() => require('./number-no-trailing-zeros'))(),
'property-allowed-list': importLazy(() => require('./property-allowed-list'))(),
'property-case': importLazy(() => require('./property-case'))(),
'property-disallowed-list': importLazy(() => require('./property-disallowed-list'))(),
'property-no-unknown': importLazy(() => require('./property-no-unknown'))(),
'property-no-vendor-prefix': importLazy(() => require('./property-no-vendor-prefix'))(),
'rule-empty-line-before': importLazy(() => require('./rule-empty-line-before'))(),
'rule-selector-property-disallowed-list': importLazy(() =>
require('./rule-selector-property-disallowed-list'),
)(),
'selector-attribute-brackets-space-inside': importLazy(() =>
require('./selector-attribute-brackets-space-inside'),
)(),
'selector-attribute-name-disallowed-list': importLazy(() =>
require('./selector-attribute-name-disallowed-list'),
)(),
'selector-attribute-operator-allowed-list': importLazy(() =>
require('./selector-attribute-operator-allowed-list'),
)(),
'selector-attribute-operator-disallowed-list': importLazy(() =>
require('./selector-attribute-operator-disallowed-list'),
)(),
'selector-attribute-operator-space-after': importLazy(() =>
require('./selector-attribute-operator-space-after'),
)(),
'selector-attribute-operator-space-before': importLazy(() =>
require('./selector-attribute-operator-space-before'),
)(),
'selector-attribute-quotes': importLazy(() => require('./selector-attribute-quotes'))(),
'selector-class-pattern': importLazy(() => require('./selector-class-pattern'))(),
'selector-combinator-allowed-list': importLazy(() =>
require('./selector-combinator-allowed-list'),
)(),
'selector-combinator-disallowed-list': importLazy(() =>
require('./selector-combinator-disallowed-list'),
)(),
'selector-combinator-space-after': importLazy(() =>
require('./selector-combinator-space-after'),
)(),
'selector-combinator-space-before': importLazy(() =>
require('./selector-combinator-space-before'),
)(),
'selector-descendant-combinator-no-non-space': importLazy(() =>
require('./selector-descendant-combinator-no-non-space'),
)(),
'selector-disallowed-list': importLazy(() => require('./selector-disallowed-list'))(),
'selector-id-pattern': importLazy(() => require('./selector-id-pattern'))(),
'selector-list-comma-newline-after': importLazy(() =>
require('./selector-list-comma-newline-after'),
)(),
'selector-list-comma-newline-before': importLazy(() =>
require('./selector-list-comma-newline-before'),
)(),
'selector-list-comma-space-after': importLazy(() =>
require('./selector-list-comma-space-after'),
)(),
'selector-list-comma-space-before': importLazy(() =>
require('./selector-list-comma-space-before'),
)(),
'selector-max-attribute': importLazy(() => require('./selector-max-attribute'))(),
'selector-max-class': importLazy(() => require('./selector-max-class'))(),
'selector-max-combinators': importLazy(() => require('./selector-max-combinators'))(),
'selector-max-compound-selectors': importLazy(() =>
require('./selector-max-compound-selectors'),
)(),
'selector-max-empty-lines': importLazy(() => require('./selector-max-empty-lines'))(),
'selector-max-id': importLazy(() => require('./selector-max-id'))(),
'selector-max-pseudo-class': importLazy(() => require('./selector-max-pseudo-class'))(),
'selector-max-specificity': importLazy(() => require('./selector-max-specificity'))(),
'selector-max-type': importLazy(() => require('./selector-max-type'))(),
'selector-max-universal': importLazy(() => require('./selector-max-universal'))(),
'selector-nested-pattern': importLazy(() => require('./selector-nested-pattern'))(),
'selector-no-qualifying-type': importLazy(() => require('./selector-no-qualifying-type'))(),
'selector-no-vendor-prefix': importLazy(() => require('./selector-no-vendor-prefix'))(),
'selector-not-notation': importLazy(() => require('./selector-not-notation'))(),
'selector-pseudo-class-allowed-list': importLazy(() =>
require('./selector-pseudo-class-allowed-list'),
)(),
'selector-pseudo-class-case': importLazy(() => require('./selector-pseudo-class-case'))(),
'selector-pseudo-class-disallowed-list': importLazy(() =>
require('./selector-pseudo-class-disallowed-list'),
)(),
'selector-pseudo-class-no-unknown': importLazy(() =>
require('./selector-pseudo-class-no-unknown'),
)(),
'selector-pseudo-class-parentheses-space-inside': importLazy(() =>
require('./selector-pseudo-class-parentheses-space-inside'),
)(),
'selector-pseudo-element-allowed-list': importLazy(() =>
require('./selector-pseudo-element-allowed-list'),
)(),
'selector-pseudo-element-case': importLazy(() => require('./selector-pseudo-element-case'))(),
'selector-pseudo-element-colon-notation': importLazy(() =>
require('./selector-pseudo-element-colon-notation'),
)(),
'selector-pseudo-element-disallowed-list': importLazy(() =>
require('./selector-pseudo-element-disallowed-list'),
)(),
'selector-pseudo-element-no-unknown': importLazy(() =>
require('./selector-pseudo-element-no-unknown'),
)(),
'selector-type-case': importLazy(() => require('./selector-type-case'))(),
'selector-type-no-unknown': importLazy(() => require('./selector-type-no-unknown'))(),
'shorthand-property-no-redundant-values': importLazy(() =>
require('./shorthand-property-no-redundant-values'),
)(),
'string-no-newline': importLazy(() => require('./string-no-newline'))(),
'string-quotes': importLazy(() => require('./string-quotes'))(),
'time-min-milliseconds': importLazy(() => require('./time-min-milliseconds'))(),
'unicode-bom': importLazy(() => require('./unicode-bom'))(),
'unit-allowed-list': importLazy(() => require('./unit-allowed-list'))(),
'unit-case': importLazy(() => require('./unit-case'))(),
'unit-disallowed-list': importLazy(() => require('./unit-disallowed-list'))(),
'unit-no-unknown': importLazy(() => require('./unit-no-unknown'))(),
'value-keyword-case': importLazy(() => require('./value-keyword-case'))(),
'value-list-comma-newline-after': importLazy(() => require('./value-list-comma-newline-after'))(),
'value-list-comma-newline-before': importLazy(() =>
require('./value-list-comma-newline-before'),
)(),
'value-list-comma-space-after': importLazy(() => require('./value-list-comma-space-after'))(),
'value-list-comma-space-before': importLazy(() => require('./value-list-comma-space-before'))(),
'value-list-max-empty-lines': importLazy(() => require('./value-list-max-empty-lines'))(),
'value-no-vendor-prefix': importLazy(() => require('./value-no-vendor-prefix'))(),
indentation: importLazy(() => require('./indentation'))(),
};
module.exports = rules;

View File

@@ -0,0 +1,64 @@
'use strict';
const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'keyframe-block-no-duplicate-selectors';
const messages = ruleMessages(ruleName, {
rejected: (selector) => `Unexpected duplicate "${selector}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/keyframe-block-no-duplicate-selectors',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual: primary });
if (!validOptions) {
return;
}
root.walkAtRules(/^(-(moz|webkit)-)?keyframes$/i, (atRuleKeyframes) => {
const selectors = new Set();
atRuleKeyframes.walkRules((keyframeRule) => {
const ruleSelectors = keyframeRule.selectors;
ruleSelectors.forEach((selector) => {
if (!isStandardSyntaxSelector(selector)) {
return;
}
const normalizedSelector = selector.toLowerCase();
const isDuplicate = selectors.has(normalizedSelector);
if (isDuplicate) {
report({
message: messages.rejected(selector),
node: keyframeRule,
result,
ruleName,
word: selector,
});
return;
}
selectors.add(normalizedSelector);
});
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,54 @@
'use strict';
const getImportantPosition = require('../../utils/getImportantPosition');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { assert } = require('../../utils/validateTypes');
const ruleName = 'keyframe-declaration-no-important';
const messages = ruleMessages(ruleName, {
rejected: 'Unexpected !important',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/keyframe-declaration-no-important',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual: primary });
if (!validOptions) {
return;
}
root.walkAtRules(/^(-(moz|webkit)-)?keyframes$/i, (atRuleKeyframes) => {
atRuleKeyframes.walkDecls((decl) => {
if (!decl.important) {
return;
}
const pos = getImportantPosition(decl.toString());
assert(pos);
report({
message: messages.rejected,
node: decl,
index: pos.index,
endIndex: pos.endIndex,
result,
ruleName,
});
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,154 @@
'use strict';
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const transformSelector = require('../../utils/transformSelector');
const validateOptions = require('../../utils/validateOptions');
const { assertString } = require('../../utils/validateTypes');
const ruleName = 'keyframe-selector-notation';
const messages = ruleMessages(ruleName, {
expected: (selector, fixedSelector) => `Expected "${selector}" to be "${fixedSelector}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/keyframe-selector-notation',
fixable: true,
};
const PERCENTAGE_SELECTORS = new Set(['0%', '100%']);
const KEYWORD_SELECTORS = new Set(['from', 'to']);
const PERCENTAGE_TO_KEYWORD = new Map([
['0%', 'from'],
['100%', 'to'],
]);
const KEYWORD_TO_PERCENTAGE = new Map([
['from', '0%'],
['to', '100%'],
]);
/** @type {import('stylelint').Rule<'keyword' | 'percentage' | 'percentage-unless-within-keyword-only-block'>} */
const rule = (primary, _, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['keyword', 'percentage', 'percentage-unless-within-keyword-only-block'],
});
if (!validOptions) return;
/**
* @typedef {{
* expFunc: (selector: string, selectorsInBlock: string[]) => boolean,
* fixFunc: (selector: string) => string,
* }} OptionFuncs
*
* @type {Record<primary, OptionFuncs>}
*/
const optionFuncs = Object.freeze({
keyword: {
expFunc: (selector) => KEYWORD_SELECTORS.has(selector),
fixFunc: (selector) => getFromMap(PERCENTAGE_TO_KEYWORD, selector),
},
percentage: {
expFunc: (selector) => PERCENTAGE_SELECTORS.has(selector),
fixFunc: (selector) => getFromMap(KEYWORD_TO_PERCENTAGE, selector),
},
'percentage-unless-within-keyword-only-block': {
expFunc: (selector, selectorsInBlock) => {
if (selectorsInBlock.every((s) => KEYWORD_SELECTORS.has(s))) return true;
return PERCENTAGE_SELECTORS.has(selector);
},
fixFunc: (selector) => getFromMap(KEYWORD_TO_PERCENTAGE, selector),
},
});
root.walkAtRules(/^(-(moz|webkit)-)?keyframes$/i, (atRuleKeyframes) => {
const selectorsInBlock =
primary === 'percentage-unless-within-keyword-only-block'
? getSelectorsInBlock(atRuleKeyframes)
: [];
atRuleKeyframes.walkRules((keyframeRule) => {
transformSelector(result, keyframeRule, (selectors) => {
selectors.walkTags((selectorTag) => {
checkSelector(
selectorTag.value,
optionFuncs[primary],
(fixedSelector) => (selectorTag.value = fixedSelector),
);
});
});
/**
* @param {string} selector
* @param {OptionFuncs} funcs
* @param {(fixedSelector: string) => void} fixer
*/
function checkSelector(selector, { expFunc, fixFunc }, fixer) {
const normalizedSelector = selector.toLowerCase();
if (
!KEYWORD_SELECTORS.has(normalizedSelector) &&
!PERCENTAGE_SELECTORS.has(normalizedSelector)
) {
return;
}
if (expFunc(selector, selectorsInBlock)) return;
const fixedSelector = fixFunc(selector);
if (context.fix) {
fixer(fixedSelector);
return;
}
report({
message: messages.expected(selector, fixedSelector),
node: keyframeRule,
result,
ruleName,
word: selector,
});
}
});
});
};
};
/**
* @param {Map<string, string>} map
* @param {string} key
* @returns {string}
*/
function getFromMap(map, key) {
const value = map.get(key);
assertString(value);
return value;
}
/**
* @param {import('postcss').AtRule} atRule
* @returns {string[]}
*/
function getSelectorsInBlock(atRule) {
/** @type {string[]} */
const selectors = [];
atRule.walkRules((r) => {
selectors.push(...r.selectors);
});
return selectors;
}
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,64 @@
'use strict';
const atRuleParamIndex = require('../../utils/atRuleParamIndex');
const isStandardSyntaxKeyframesName = require('../../utils/isStandardSyntaxKeyframesName');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'keyframes-name-pattern';
const messages = ruleMessages(ruleName, {
expected: (keyframeName, pattern) => `Expected "${keyframeName}" to match pattern "${pattern}"`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/keyframes-name-pattern',
};
/** @type {import('stylelint').Rule} */
const rule = (primary) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: [isRegExp, isString],
});
if (!validOptions) {
return;
}
const regex = isString(primary) ? new RegExp(primary) : primary;
root.walkAtRules(/keyframes/i, (keyframesNode) => {
const value = keyframesNode.params;
if (!isStandardSyntaxKeyframesName(value)) {
return;
}
if (regex.test(value)) {
return;
}
const index = atRuleParamIndex(keyframesNode);
const endIndex = index + value.length;
report({
index,
endIndex,
message: messages.expected,
messageArgs: [value, primary],
node: keyframesNode,
ruleName,
result,
});
});
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

View File

@@ -0,0 +1,227 @@
'use strict';
const valueParser = require('postcss-value-parser');
const atRuleParamIndex = require('../../utils/atRuleParamIndex');
const declarationValueIndex = require('../../utils/declarationValueIndex');
const getAtRuleParams = require('../../utils/getAtRuleParams');
const getDeclarationValue = require('../../utils/getDeclarationValue');
const isCustomProperty = require('../../utils/isCustomProperty');
const isMathFunction = require('../../utils/isMathFunction');
const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
const { lengthUnits } = require('../../reference/units');
const optionsMatches = require('../../utils/optionsMatches');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const setAtRuleParams = require('../../utils/setAtRuleParams');
const setDeclarationValue = require('../../utils/setDeclarationValue');
const validateOptions = require('../../utils/validateOptions');
const { isRegExp, isString } = require('../../utils/validateTypes');
const ruleName = 'length-zero-no-unit';
const messages = ruleMessages(ruleName, {
rejected: 'Unexpected unit',
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/length-zero-no-unit',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(
result,
ruleName,
{
actual: primary,
},
{
actual: secondaryOptions,
possible: {
ignore: ['custom-properties'],
ignoreFunctions: [isString, isRegExp],
},
optional: true,
},
);
if (!validOptions) return;
let needsFix;
/**
* @param {import('postcss').Node} node
* @param {number} nodeIndex
* @param {import('postcss-value-parser').Node} valueNode
*/
function check(node, nodeIndex, valueNode) {
const { value, sourceIndex } = valueNode;
if (isMathFunction(valueNode)) return false;
if (isFunction(valueNode) && optionsMatches(secondaryOptions, 'ignoreFunctions', value))
return false;
if (!isWord(valueNode)) return;
const numberUnit = valueParser.unit(value);
if (numberUnit === false) return;
const { number, unit } = numberUnit;
if (unit === '') return;
if (!isLength(unit)) return;
if (isFraction(unit)) return;
if (!isZero(number)) return;
if (context.fix) {
let regularNumber = number;
if (regularNumber.startsWith('.')) {
regularNumber = number.slice(1);
}
valueNode.value = regularNumber;
needsFix = true;
return;
}
const index = nodeIndex + sourceIndex + number.length;
const endIndex = index + unit.length;
report({
index,
endIndex,
message: messages.rejected,
node,
result,
ruleName,
});
}
/**
* @param {import('postcss').AtRule} node
*/
function checkAtRule(node) {
if (!isStandardSyntaxAtRule(node)) return;
needsFix = false;
const index = atRuleParamIndex(node);
const parsedValue = valueParser(getAtRuleParams(node));
parsedValue.walk((valueNode) => check(node, index, valueNode));
if (needsFix) {
setAtRuleParams(node, parsedValue.toString());
}
}
/**
* @param {import('postcss').Declaration} node
*/
function checkDecl(node) {
needsFix = false;
const { prop } = node;
if (isLineHeight(prop)) return;
if (isFlex(prop)) return;
if (optionsMatches(secondaryOptions, 'ignore', 'custom-properties') && isCustomProperty(prop))
return;
const index = declarationValueIndex(node);
const parsedValue = valueParser(getDeclarationValue(node));
parsedValue.walk((valueNode, valueNodeIndex, valueNodes) => {
if (isLineHeightValue(node, valueNodes, valueNodeIndex)) return;
return check(node, index, valueNode);
});
if (needsFix) {
setDeclarationValue(node, parsedValue.toString());
}
}
root.walkAtRules(checkAtRule);
root.walkDecls(checkDecl);
};
};
/**
* @param {import('postcss').Declaration} decl
* @param {import('postcss-value-parser').Node[]} nodes
* @param {number} index
*/
function isLineHeightValue({ prop }, nodes, index) {
const lastNode = nodes[index - 1];
return (
prop.toLowerCase() === 'font' && lastNode && lastNode.type === 'div' && lastNode.value === '/'
);
}
/**
* @param {string} prop
*/
function isLineHeight(prop) {
return prop.toLowerCase() === 'line-height';
}
/**
* @param {string} prop
*/
function isFlex(prop) {
return prop.toLowerCase() === 'flex';
}
/**
* @param {import('postcss-value-parser').Node} node
*/
function isWord({ type }) {
return type === 'word';
}
/**
* @param {string} unit
*/
function isLength(unit) {
return lengthUnits.has(unit.toLowerCase());
}
/**
* @param {import('postcss-value-parser').Node} node
*/
function isFunction({ type }) {
return type === 'function';
}
/**
* @param {string} unit
*/
function isFraction(unit) {
return unit.toLowerCase() === 'fr';
}
/**
* @param {string} number
*/
function isZero(number) {
return Number.parseFloat(number) === 0;
}
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

131
node_modules/stylelint/lib/rules/linebreaks/index.js generated vendored Normal file
View File

@@ -0,0 +1,131 @@
'use strict';
const postcss = require('postcss');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const ruleName = 'linebreaks';
const messages = ruleMessages(ruleName, {
expected: (linebreak) => `Expected linebreak to be ${linebreak}`,
});
const meta = {
url: 'https://stylelint.io/user-guide/rules/linebreaks',
fixable: true,
};
/** @type {import('stylelint').Rule} */
const rule = (primary, _secondaryOptions, context) => {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, {
actual: primary,
possible: ['unix', 'windows'],
});
if (!validOptions) {
return;
}
const shouldHaveCR = primary === 'windows';
if (context.fix) {
root.walk((node) => {
if ('selector' in node) {
node.selector = fixData(node.selector);
}
if ('value' in node) {
node.value = fixData(node.value);
}
if ('text' in node) {
node.text = fixData(node.text);
}
if (node.raws.before) {
node.raws.before = fixData(node.raws.before);
}
if (typeof node.raws.after === 'string') {
node.raws.after = fixData(node.raws.after);
}
});
if (typeof root.raws.after === 'string') {
root.raws.after = fixData(root.raws.after);
}
} else {
if (root.source == null) throw new Error('The root node must have a source');
const lines = root.source.input.css.split('\n');
for (let [i, line] of lines.entries()) {
if (i < lines.length - 1 && !line.includes('\r')) {
line += '\n';
}
if (hasError(line)) {
const lineNum = i + 1;
const colNum = line.length;
reportNewlineError(lineNum, colNum);
}
}
}
/**
* @param {string} dataToCheck
*/
function hasError(dataToCheck) {
const hasNewlineToVerify = /[\r\n]/.test(dataToCheck);
const hasCR = hasNewlineToVerify ? /\r/.test(dataToCheck) : false;
return hasNewlineToVerify && hasCR !== shouldHaveCR;
}
/**
* @param {string} data
*/
function fixData(data) {
if (data) {
let res = data.replace(/\r/g, '');
if (shouldHaveCR) {
res = res.replace(/\n/g, '\r\n');
}
return res;
}
return data;
}
/**
* @param {number} line
* @param {number} column
*/
function reportNewlineError(line, column) {
// Creating a node manually helps us to point to empty lines.
const node = postcss.rule({
source: {
start: { line, column, offset: 0 },
input: new postcss.Input(''),
},
});
report({
message: messages.expected(primary),
node,
result,
ruleName,
});
}
};
};
rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;

Some files were not shown because too many files have changed in this diff Show More