stylelint-less
Version:
A collection of Less specific rules for stylelint
344 lines (292 loc) • 8.21 kB
JavaScript
;
var stylelint = require('stylelint');
var valueParser = require('postcss-value-parser');
var postcss = require('postcss');
const prefix = 'less';
function namespace(ruleName) {
return `${prefix}/${ruleName}`;
}
/**
* Check whether the atrule is valid less variable.
*
* @param {import('postcss').AtRule} atRule
* @returns {boolean}
*/
function isValidVariable(atRule) {
// support `each` - http://lesscss.org/functions/#list-functions-each
if (atRule.name === 'each') {
return true;
}
return !!(('variable' in atRule && atRule.raws.afterName.includes(':')) || atRule.mixin);
}
const HEX = /^#[0-9A-Za-z]+/;
function isHexColor({ type, value }) {
return type === 'word' && HEX.test(value);
}
const IGNORED_FUNCTIONS = new Set(['url']);
function isIgnoredFunction({ type, value }) {
return type === 'function' && IGNORED_FUNCTIONS.has(value.toLowerCase());
}
/**
* Check whether an at-rule is standard
*
* @param {import('postcss').AtRule | import('postcss-less').AtRule} atRule postcss at-rule node
* @return {boolean} If `true`, the declaration is standard
* @package stylelint
*/
function isStandardSyntaxAtRule(atRule) {
// Ignore scss `@content` inside mixins
if (!atRule.nodes && atRule.params === '') {
return false;
}
// Ignore Less mixins
if ('mixin' in atRule && atRule.mixin) {
return false;
}
// Ignore Less detached ruleset `@detached-ruleset: { background: red; }; .top { @detached-ruleset(); }`
if (
('variable' in atRule && atRule.variable) ||
(!atRule.nodes && atRule.raws.afterName === '' && atRule.params[0] === '(')
) {
return false;
}
return true;
}
const IS_VALID_HEX = /^#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/i;
/**
* Check if a value is a valid 3, 4, 6 or 8 digit hex
*
* @param {string} value
* @returns {boolean}
* @package stylelint
*/
function isValidHex(value) {
return IS_VALID_HEX.test(value);
}
const ruleName$3 = namespace('color-no-hex');
const messages$3 = stylelint.utils.ruleMessages(ruleName$3, {
rejected: function (hex) {
return `Unexpected hex color "${hex}"`;
},
invalid: function (variableName) {
return `invalid variable "${variableName}"`;
},
});
function colorNoHex (actual) {
return function (root, result) {
const validOptions = stylelint.utils.validateOptions(result, ruleName$3, { actual });
if (!validOptions) {
return;
}
root.walkAtRules(function (node) {
if (!isStandardSyntaxAtRule(node)) {
if (!isValidVariable(node)) {
stylelint.utils.report({
result,
ruleName: ruleName$3,
message: messages$3.invalid(node.name),
node: node,
word: node.name,
});
} else {
const parsedValue = valueParser(node.params);
parsedValue.walk((parsedValueNode) => {
if (isIgnoredFunction(parsedValueNode)) return false;
if (!isHexColor(parsedValueNode)) return;
stylelint.utils.report({
message: messages$3.rejected(parsedValueNode.value),
node: node,
word: parsedValueNode.value,
result,
ruleName: ruleName$3,
});
});
}
}
});
};
}
const ruleName$2 = namespace('color-no-invalid-hex');
const messages$2 = stylelint.utils.ruleMessages(ruleName$2, {
rejected: function (value) {
return `Unexpected invalid hex color "${value}"`;
},
invalid: function (variableName) {
return `invalid variable "${variableName}"`;
},
});
function colorNoInvalidHex (actual) {
return function (root, result) {
const validOptions = stylelint.utils.validateOptions(result, ruleName$2, { actual });
if (!validOptions) {
return;
}
root.walkAtRules(function (node) {
if (!isStandardSyntaxAtRule(node)) {
if (!isValidVariable(node)) {
stylelint.utils.report({
result,
ruleName: ruleName$2,
message: messages$2.invalid(node.name),
node: node,
word: node.name,
});
} else {
valueParser(node.params).walk(({ value, type }) => {
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 (isValidHex(hexValue)) return;
stylelint.utils.report({
message: messages$2.rejected(hexValue),
node: node,
word: node.params,
result,
ruleName: ruleName$2,
});
});
}
}
});
};
}
const ruleName$1 = namespace('color-hex-case');
const messages$1 = stylelint.utils.ruleMessages(ruleName$1, {
rejected: function (actual, expected) {
return `Expected "${actual}" to be "${expected}"`;
},
invalid: function (variableName) {
return `invalid variable "${variableName}"`;
},
});
function colorHexCase (expectation) {
return function (root, result) {
const validOptions = stylelint.utils.validateOptions(result, ruleName$1, {
actual: expectation,
possible: ['lower', 'upper'],
});
if (!validOptions) {
return;
}
root.walkAtRules(function (node) {
const n = postcss.atRule(node);
if (!isStandardSyntaxAtRule(n)) {
if (!isValidVariable(n)) {
stylelint.utils.report({
result,
ruleName: ruleName$1,
message: messages$1.invalid(n.name),
node: n,
word: n.name,
});
} else {
const parsedValue = valueParser(n.params);
parsedValue.walk((parsedValueNode) => {
if (isIgnoredFunction(parsedValueNode)) return false;
if (!isHexColor(parsedValueNode)) return;
const expectedValue =
expectation === 'lower'
? parsedValueNode.value.toLowerCase()
: parsedValueNode.value.toUpperCase();
if (parsedValueNode.value === expectedValue) return;
stylelint.utils.report({
message: messages$1.rejected(parsedValueNode.value, expectedValue),
node: n,
word: parsedValueNode.value,
result,
ruleName: ruleName$1,
});
});
}
}
});
};
}
const ruleName = namespace('no-duplicate-variables');
const messages = stylelint.utils.ruleMessages(ruleName, {
rejected: function (prop) {
return `unexpected duplicate property in "${prop}"`;
},
invalid: function (variableName) {
return `Unexpected Invalid variable "${variableName}"`;
},
});
function noDuplicateVariables (actual) {
return function (root, result) {
const validOptions = stylelint.utils.validateOptions(result, ruleName, { actual });
if (!validOptions) {
return;
}
let globalVariables = [];
root.walkRules((rule) => {
let variables = [];
rule.nodes.forEach(function (node) {
if (node.type === 'atrule') {
if (!isStandardSyntaxAtRule(node)) {
if (!isValidVariable(node)) {
stylelint.utils.report({
result,
ruleName,
message: messages.invalid(node.name),
node: node,
word: node.name,
});
} else {
if (variables.includes(node.name)) {
stylelint.utils.report({
result,
ruleName,
message: messages.rejected(node.name),
node: node,
word: node.name,
});
} else {
variables.push(node.name);
}
}
}
}
});
});
root.walkAtRules((node) => {
//check duplicate in global variables
if (node.parent.type === 'root') {
if (!isStandardSyntaxAtRule(node)) {
if (!isValidVariable(node)) {
stylelint.utils.report({
result,
ruleName,
message: messages.invalid(node.name),
node: node,
word: node.name,
});
} else {
if (globalVariables.includes(node.name)) {
stylelint.utils.report({
result,
ruleName,
message: messages.rejected(node.name),
node: node,
word: node.name,
});
} else {
globalVariables.push(node.name);
}
}
}
}
});
};
}
var rules = {
'color-no-hex': colorNoHex,
'color-no-invalid-hex': colorNoInvalidHex,
'no-duplicate-variables': noDuplicateVariables,
'color-hex-case': colorHexCase,
};
var index = Object.keys(rules).map((ruleName) => {
return stylelint.createPlugin(namespace(ruleName), rules[ruleName]);
});
module.exports = index;