UNPKG

3.96 kBJavaScriptView Raw
1'use strict';
2
3const declarationValueIndex = require('../../utils/declarationValueIndex');
4const isStandardSyntaxFunction = require('../../utils/isStandardSyntaxFunction');
5const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
6const optionsMatches = require('../../utils/optionsMatches');
7const propertySets = require('../../reference/propertySets');
8const report = require('../../utils/report');
9const ruleMessages = require('../../utils/ruleMessages');
10const validateOptions = require('../../utils/validateOptions');
11const valueParser = require('postcss-value-parser');
12const { isRegExp, isString } = require('../../utils/validateTypes');
13const { colord } = require('./colordUtils');
14
15const ruleName = 'color-named';
16
17const messages = ruleMessages(ruleName, {
18 expected: (named, original) => `Expected "${original}" to be "${named}"`,
19 rejected: (named) => `Unexpected named color "${named}"`,
20});
21
22const meta = {
23 url: 'https://stylelint.io/user-guide/rules/list/color-named',
24};
25
26// Todo tested on case insensitivity
27const NODE_TYPES = new Set(['word', 'function']);
28
29/** @type {import('stylelint').Rule} */
30const rule = (primary, secondaryOptions) => {
31 return (root, result) => {
32 const validOptions = validateOptions(
33 result,
34 ruleName,
35 {
36 actual: primary,
37 possible: ['never', 'always-where-possible'],
38 },
39 {
40 actual: secondaryOptions,
41 possible: {
42 ignoreProperties: [isString, isRegExp],
43 ignore: ['inside-function'],
44 },
45 optional: true,
46 },
47 );
48
49 if (!validOptions) {
50 return;
51 }
52
53 root.walkDecls((decl) => {
54 if (propertySets.acceptCustomIdents.has(decl.prop)) {
55 return;
56 }
57
58 // Return early if the property is to be ignored
59 if (optionsMatches(secondaryOptions, 'ignoreProperties', decl.prop)) {
60 return;
61 }
62
63 valueParser(decl.value).walk((node) => {
64 const value = node.value;
65 const type = node.type;
66 const sourceIndex = node.sourceIndex;
67
68 if (optionsMatches(secondaryOptions, 'ignore', 'inside-function') && type === 'function') {
69 return false;
70 }
71
72 if (!isStandardSyntaxFunction(node)) {
73 return false;
74 }
75
76 if (!isStandardSyntaxValue(value)) {
77 return;
78 }
79
80 // Return early if neither a word nor a function
81 if (!NODE_TYPES.has(type)) {
82 return;
83 }
84
85 // Check for named colors for "never" option
86 if (
87 primary === 'never' &&
88 type === 'word' &&
89 /^[a-z]+$/iu.test(value) &&
90 value.toLowerCase() !== 'transparent' &&
91 colord(value).isValid()
92 ) {
93 complain(messages.rejected(value), decl, declarationValueIndex(decl) + sourceIndex);
94
95 return;
96 }
97
98 // Check "always-where-possible" option ...
99 if (primary !== 'always-where-possible') {
100 return;
101 }
102
103 let colorString = null;
104
105 if (type === 'function') {
106 // First by checking for alternative color function representations ...
107 // Remove all spaces to match what's in `representations`
108 colorString = valueParser
109 .stringify(node)
110 .replace(/\s*([,/()])\s*/g, '$1')
111 .replace(/\s{2,}/g, ' ');
112 } else if (type === 'word' && value.startsWith('#')) {
113 // Then by checking for alternative hex representations
114 colorString = value;
115 } else {
116 return;
117 }
118
119 const color = colord(colorString);
120
121 if (!color.isValid()) {
122 return;
123 }
124
125 const namedColor = color.toName();
126
127 if (namedColor && namedColor.toLowerCase() !== 'transparent') {
128 complain(
129 messages.expected(namedColor, colorString),
130 decl,
131 declarationValueIndex(decl) + sourceIndex,
132 );
133 }
134 });
135 });
136
137 /**
138 * @param {string} message
139 * @param {import('postcss').Node} node
140 * @param {number} index
141 */
142 function complain(message, node, index) {
143 report({
144 result,
145 ruleName,
146 message,
147 node,
148 index,
149 });
150 }
151 };
152};
153
154rule.ruleName = ruleName;
155rule.messages = messages;
156rule.meta = meta;
157module.exports = rule;