UNPKG

stylelint-less

Version:

A collection of Less specific rules for stylelint

344 lines (292 loc) 8.21 kB
'use strict'; 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;