index.js 6.3 KB

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