diff --git a/Readme.md b/Readme.md index 5944116..a1e21eb 100644 --- a/Readme.md +++ b/Readme.md @@ -3,7 +3,6 @@ [![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors) [![codecov](https://codecov.io/gh/productboardlabs/stylelint-pb/branch/master/graph/badge.svg)](https://codecov.io/gh/productboardlabs/stylelint-pb) - Set (TODO) of our custom made rules for Stylelint. 🚀 ## Story @@ -40,6 +39,18 @@ Example configuration. 👇 } ``` +You can also run this rule in **strict mode** which means that there is no other color than variable allowed! + +```js +{ + rules: { + "@productboardlabs/smart-color-replacement": [{ + "@snowWhite": "#f4f5e2" + }, "strictMode"] + } +} +``` + ## Contributors Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): diff --git a/src/index.js b/src/index.js index 63a9e13..107cb61 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,6 @@ const { createPlugin } = require("stylelint"); -const rule = require("./rules"); +const rules = require("./rules"); -module.exports = createPlugin("@productboard/smart-color-replacement", rule); +module.exports = Object.values(rules).map(rule => + createPlugin(rule.ruleName, rule) +); diff --git a/src/rules/__tests__/index.test.js b/src/rules/__tests__/index.test.js deleted file mode 100644 index 2f0e9a7..0000000 --- a/src/rules/__tests__/index.test.js +++ /dev/null @@ -1,53 +0,0 @@ -const rule = require("../index"); - -testRule(rule, { - ruleName: rule.ruleName, - config: [ - { - "@snowWhite": "#f4f5e2" - } - ], - fix: true, - - accept: [ - { - code: "a { color: @snowWhite; }", - description: "Variables are OK" - }, - { - code: "a { font-size: 3rem; }", - description: "No variable is OK" - } - ], - - reject: [ - { - code: "a { color: #f4f5e2; }", - fixed: "a { color: @snowWhite; }", - description: "Variables should be used", - message: - 'Colour "#f4f5e2" should be "@snowWhite". (@productboard/smart-color-replacement)', - line: 1 - }, - { - code: "a { background: rgb(244, 245, 226); }", - fixed: "a { background: @snowWhite; }", - description: "Variables should be used", - message: - 'Colour "rgb(244, 245, 226)" should be "@snowWhite". (@productboard/smart-color-replacement)', - line: 1 - } - ] -}); - -testRule(rule, { - ruleName: rule.ruleName, - fix: true, - - accept: [ - { - code: "a { font-size: 3rem; }", - description: "No variable is OK" - } - ] -}); diff --git a/src/rules/__tests__/smart-color-replacement.test.js b/src/rules/__tests__/smart-color-replacement.test.js new file mode 100644 index 0000000..45791be --- /dev/null +++ b/src/rules/__tests__/smart-color-replacement.test.js @@ -0,0 +1,100 @@ +const rule = require("../smart-color-replacement"); + +testRule(rule, { + ruleName: rule.ruleName, + config: [ + { + "@snowWhite": "#f4f5e2" + } + ], + fix: true, + + accept: [ + { + code: "", + description: "empty stylesheet" + }, + { + code: "a {}", + description: "empty rule" + }, + { + code: '@import "foo.css";', + description: "blockless statement" + }, + { + code: ":global {}", + description: "CSS Modules global empty rule set" + }, + { + code: "a { color: @snowWhite; }", + description: "Usage of correct variable" + }, + { + code: "a { font-size: 3rem; }", + description: "No color usage should be ignored" + }, + { + code: "a { color: #333333; }", + description: "Color out of the config should be ignored by default" + } + ], + + reject: [ + { + code: "a { color: #f4f5e2; }", + fixed: "a { color: @snowWhite; }", + description: "Should use correct variable not hexadecimal notation", + message: rule.messages.expected([ + { + used: "#f4f5e2", + suggested: "@snowWhite" + } + ]), + line: 1 + }, + { + code: "a { background: rgb(244, 245, 226); }", + fixed: "a { background: @snowWhite; }", + description: "Should use correct variable not rgb notation", + message: rule.messages.expected([ + { + used: "rgb(244, 245, 226)", + suggested: "@snowWhite" + } + ]), + line: 1 + } + ] +}); + +testRule(rule, { + ruleName: rule.ruleName, + fix: true, + + accept: [ + { + code: "a { font-size: 3rem; }", + description: "Should do nothing without config" + } + ] +}); + +testRule(rule, { + ruleName: rule.ruleName, + config: [ + { + "@snowWhite": "#f4f5e2" + }, + "strictMode" + ], + fix: true, + + reject: [ + { + code: "a { color: #333333; }", + fixed: "a { color: #333333; }", + description: "Any color except variables is prohibited!" + } + ] +}); diff --git a/src/rules/index.js b/src/rules/index.js index 9a531d3..e490185 100644 --- a/src/rules/index.js +++ b/src/rules/index.js @@ -1,78 +1,5 @@ -const stylelint = require("stylelint"); -const color = require("color"); +const smartColorReplacement = require("./smart-color-replacement"); -const ruleName = "@productboard/smart-color-replacement"; - -const messages = stylelint.utils.ruleMessages(ruleName, { - expected: failedColors => - failedColors - .map( - ({ used, suggested }) => `Colour "${used}" should be "${suggested}".` - ) - .join(" ") -}); - -const rule = function(expectation, _, context) { - return (root, result) => { - const HEX_REGEX = /(#[a-f0-9]{3,6})/gi; - const RGB_REGEX = /(rgb|rgba)\([^\)]*\)/gi; - const HSL_REGEX = /hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/gi; - - const patterns = [HEX_REGEX, RGB_REGEX, HSL_REGEX]; - - if (typeof expectation !== "object" || !expectation) return; - - root.walkDecls(declaration => { - const { value } = declaration; - const colors = patterns.reduce((acc, c) => { - const matches = value.match(c); - if (matches && matches.length > 0) { - const colors = matches.map(m => ({ - sanitized: color(m).hex(), - original: m - })); - acc.push(...colors); - } - - return acc; - }, []); - - const lookUpObject = Object.entries(expectation).reduce( - (acc, [key, value]) => { - acc[value.toUpperCase()] = key; - - return acc; - }, - {} - ); - - const results = colors.map(({ sanitized, original }) => ({ - valid: !lookUpObject[sanitized], - used: original, - suggested: lookUpObject[sanitized] - })); - - const failedColors = results.filter(res => !res.valid); - - if (failedColors.length) { - stylelint.utils.report({ - message: messages.expected(failedColors), - node: declaration, - result, - ruleName - }); - - if (context.fix) { - failedColors.forEach(({ used, suggested }) => { - declaration.value = declaration.value.replace(used, suggested); - }); - } - } - }); - }; +module.exports = { + smartColorReplacement }; - -rule.ruleName = ruleName; -rule.messages = messages; - -module.exports = rule; diff --git a/src/rules/smart-color-replacement.js b/src/rules/smart-color-replacement.js new file mode 100644 index 0000000..bc8b094 --- /dev/null +++ b/src/rules/smart-color-replacement.js @@ -0,0 +1,79 @@ +const stylelint = require("stylelint"); +const color = require("color"); + +const HEX_REGEX = /(#[a-f0-9]{3,6})/gi; +const RGB_REGEX = /(rgb|rgba)\([^\)]*\)/gi; +const HSL_REGEX = /hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/gi; + +const ruleName = "@productboard/smart-color-replacement"; + +const messages = stylelint.utils.ruleMessages(ruleName, { + expected: failedColors => + failedColors + .map(({ used, suggested }) => `Color "${used}" should be "${suggested}".`) + .join(" ") +}); + +const rule = function(configuration, strictMode, context) { + return (root, result) => { + if (typeof configuration !== "object" || !configuration) return; + + const lookUpObject = Object.entries(configuration).reduce( + (acc, [key, value]) => { + acc[value.toUpperCase()] = key; + + return acc; + }, + {} + ); + + const patterns = [HEX_REGEX, RGB_REGEX, HSL_REGEX]; + + root.walkDecls(declaration => { + const { value } = declaration; + const colors = patterns.reduce((acc, c) => { + const matches = value.match(c); + if (matches && matches.length > 0) { + acc.push(...matches); + } + + return acc; + }, []); + + const results = colors.map(used => { + const sanitized = color(used).hex(); + return { + valid: lookUpObject[sanitized] + ? !lookUpObject[sanitized] + : strictMode !== "strictMode", + suggested: lookUpObject[sanitized], + used + }; + }); + + const failedColors = results.filter(res => !res.valid); + + if (failedColors.length) { + stylelint.utils.report({ + message: messages.expected(failedColors), + node: declaration, + result, + ruleName + }); + + if (context.fix) { + failedColors + .filter(({ suggested }) => suggested) + .forEach(({ used, suggested }) => { + declaration.value = declaration.value.replace(used, suggested); + }); + } + } + }); + }; +}; + +rule.ruleName = ruleName; +rule.messages = messages; + +module.exports = rule;