UNPKG

5.69 kBJavaScriptView Raw
1'use strict';
2
3const _ = require('lodash');
4const declarationValueIndex = require('../../utils/declarationValueIndex');
5const getUnitFromValueNode = require('../../utils/getUnitFromValueNode');
6const isCounterIncrementCustomIdentValue = require('../../utils/isCounterIncrementCustomIdentValue');
7const isCounterResetCustomIdentValue = require('../../utils/isCounterResetCustomIdentValue');
8const isStandardSyntaxValue = require('../../utils/isStandardSyntaxValue');
9const keywordSets = require('../../reference/keywordSets');
10const matchesStringOrRegExp = require('../../utils/matchesStringOrRegExp');
11const report = require('../../utils/report');
12const ruleMessages = require('../../utils/ruleMessages');
13const validateOptions = require('../../utils/validateOptions');
14const valueParser = require('postcss-value-parser');
15
16const ruleName = 'value-keyword-case';
17
18const messages = ruleMessages(ruleName, {
19 expected: (actual, expected) => `Expected "${actual}" to be "${expected}"`,
20});
21
22// Operators are interpreted as "words" by the value parser, so we want to make sure to ignore them.
23const ignoredCharacters = new Set(['+', '-', '/', '*', '%']);
24const gridRowProps = new Set(['grid-row', 'grid-row-start', 'grid-row-end']);
25const gridColumnProps = new Set(['grid-column', 'grid-column-start', 'grid-column-end']);
26
27const mapLowercaseKeywordsToCamelCase = new Map();
28
29keywordSets.camelCaseKeywords.forEach((func) => {
30 mapLowercaseKeywordsToCamelCase.set(func.toLowerCase(), func);
31});
32
33function rule(expectation, options, context) {
34 return (root, result) => {
35 const validOptions = validateOptions(
36 result,
37 ruleName,
38 {
39 actual: expectation,
40 possible: ['lower', 'upper'],
41 },
42 {
43 actual: options,
44 possible: {
45 ignoreProperties: [_.isString, _.isRegExp],
46 ignoreKeywords: [_.isString, _.isRegExp],
47 },
48 optional: true,
49 },
50 );
51
52 if (!validOptions) {
53 return;
54 }
55
56 root.walkDecls((decl) => {
57 const prop = decl.prop.toLowerCase();
58 const value = decl.value;
59
60 const parsed = valueParser(decl.raws.value ? decl.raws.value.raw : decl.value);
61
62 let needFix = false;
63
64 parsed.walk((node) => {
65 const valueLowerCase = node.value.toLowerCase();
66
67 // Ignore system colors
68 if (keywordSets.systemColors.has(valueLowerCase)) {
69 return;
70 }
71
72 // Ignore keywords within `url` and `var` function
73 if (
74 node.type === 'function' &&
75 (valueLowerCase === 'url' ||
76 valueLowerCase === 'var' ||
77 valueLowerCase === 'counter' ||
78 valueLowerCase === 'counters' ||
79 valueLowerCase === 'attr')
80 ) {
81 return false;
82 }
83
84 const keyword = node.value;
85
86 // Ignore css variables, and hex values, and math operators, and sass interpolation
87 if (
88 node.type !== 'word' ||
89 !isStandardSyntaxValue(node.value) ||
90 value.includes('#') ||
91 ignoredCharacters.has(keyword) ||
92 getUnitFromValueNode(node)
93 ) {
94 return;
95 }
96
97 if (
98 prop === 'animation' &&
99 !keywordSets.animationShorthandKeywords.has(valueLowerCase) &&
100 !keywordSets.animationNameKeywords.has(valueLowerCase)
101 ) {
102 return;
103 }
104
105 if (prop === 'animation-name' && !keywordSets.animationNameKeywords.has(valueLowerCase)) {
106 return;
107 }
108
109 if (
110 prop === 'font' &&
111 !keywordSets.fontShorthandKeywords.has(valueLowerCase) &&
112 !keywordSets.fontFamilyKeywords.has(valueLowerCase)
113 ) {
114 return;
115 }
116
117 if (prop === 'font-family' && !keywordSets.fontFamilyKeywords.has(valueLowerCase)) {
118 return;
119 }
120
121 if (prop === 'counter-increment' && isCounterIncrementCustomIdentValue(valueLowerCase)) {
122 return;
123 }
124
125 if (prop === 'counter-reset' && isCounterResetCustomIdentValue(valueLowerCase)) {
126 return;
127 }
128
129 if (gridRowProps.has(prop) && !keywordSets.gridRowKeywords.has(valueLowerCase)) {
130 return;
131 }
132
133 if (gridColumnProps.has(prop) && !keywordSets.gridColumnKeywords.has(valueLowerCase)) {
134 return;
135 }
136
137 if (prop === 'grid-area' && !keywordSets.gridAreaKeywords.has(valueLowerCase)) {
138 return;
139 }
140
141 if (
142 prop === 'list-style' &&
143 !keywordSets.listStyleShorthandKeywords.has(valueLowerCase) &&
144 !keywordSets.listStyleTypeKeywords.has(valueLowerCase)
145 ) {
146 return;
147 }
148
149 if (prop === 'list-style-type' && !keywordSets.listStyleTypeKeywords.has(valueLowerCase)) {
150 return;
151 }
152
153 const ignoreKeywords = (options && options.ignoreKeywords) || [];
154 const ignoreProperties = (options && options.ignoreProperties) || [];
155
156 if (ignoreKeywords.length > 0 && matchesStringOrRegExp(keyword, ignoreKeywords)) {
157 return;
158 }
159
160 if (ignoreProperties.length > 0 && matchesStringOrRegExp(prop, ignoreProperties)) {
161 return;
162 }
163
164 const keywordLowerCase = keyword.toLocaleLowerCase();
165 let expectedKeyword = null;
166
167 if (expectation === 'lower' && mapLowercaseKeywordsToCamelCase.has(keywordLowerCase)) {
168 expectedKeyword = mapLowercaseKeywordsToCamelCase.get(keywordLowerCase);
169 } else if (expectation === 'lower') {
170 expectedKeyword = keyword.toLowerCase();
171 } else {
172 expectedKeyword = keyword.toUpperCase();
173 }
174
175 if (keyword === expectedKeyword) {
176 return;
177 }
178
179 if (context.fix) {
180 needFix = true;
181 node.value = expectedKeyword;
182
183 return;
184 }
185
186 report({
187 message: messages.expected(keyword, expectedKeyword),
188 node: decl,
189 index: declarationValueIndex(decl) + node.sourceIndex,
190 result,
191 ruleName,
192 });
193 });
194
195 if (context.fix && needFix) {
196 decl.value = parsed.toString();
197 }
198 });
199 };
200}
201
202rule.ruleName = ruleName;
203rule.messages = messages;
204module.exports = rule;