index.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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/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. if (!isStandardSyntaxValue(value)) return;
  72. const parsed = valueParser(getDeclarationValue(decl));
  73. let needFix = false;
  74. parsed.walk((node) => {
  75. const keyword = node.value;
  76. const valueLowerCase = keyword.toLowerCase();
  77. // Ignore system colors
  78. if (systemColorsKeywords.has(valueLowerCase)) {
  79. return;
  80. }
  81. // Ignore keywords within `url` and `var` function
  82. if (
  83. node.type === 'function' &&
  84. (valueLowerCase === 'url' ||
  85. valueLowerCase === 'var' ||
  86. valueLowerCase === 'counter' ||
  87. valueLowerCase === 'counters' ||
  88. valueLowerCase === 'attr')
  89. ) {
  90. return false;
  91. }
  92. // ignore keywords within ignoreFunctions functions
  93. if (
  94. node.type === 'function' &&
  95. optionsMatches(secondaryOptions, 'ignoreFunctions', keyword)
  96. ) {
  97. return false;
  98. }
  99. const { unit } = getDimension(node);
  100. // Ignore css variables, and hex values, and math operators, and sass interpolation
  101. if (
  102. node.type !== 'word' ||
  103. !isStandardSyntaxValue(keyword) ||
  104. value.includes('#') ||
  105. ignoredCharacters.has(keyword) ||
  106. unit
  107. ) {
  108. return;
  109. }
  110. if (
  111. propLowerCase === 'animation' &&
  112. !animationShorthandKeywords.has(valueLowerCase) &&
  113. !animationNameKeywords.has(valueLowerCase)
  114. ) {
  115. return;
  116. }
  117. if (propLowerCase === 'animation-name' && !animationNameKeywords.has(valueLowerCase)) {
  118. return;
  119. }
  120. if (
  121. propLowerCase === 'font' &&
  122. !fontShorthandKeywords.has(valueLowerCase) &&
  123. !fontFamilyKeywords.has(valueLowerCase)
  124. ) {
  125. return;
  126. }
  127. if (propLowerCase === 'font-family' && !fontFamilyKeywords.has(valueLowerCase)) {
  128. return;
  129. }
  130. if (
  131. propLowerCase === 'counter-increment' &&
  132. isCounterIncrementCustomIdentValue(valueLowerCase)
  133. ) {
  134. return;
  135. }
  136. if (propLowerCase === 'counter-reset' && isCounterResetCustomIdentValue(valueLowerCase)) {
  137. return;
  138. }
  139. if (gridRowProps.has(propLowerCase) && !gridRowKeywords.has(valueLowerCase)) {
  140. return;
  141. }
  142. if (gridColumnProps.has(propLowerCase) && !gridColumnKeywords.has(valueLowerCase)) {
  143. return;
  144. }
  145. if (propLowerCase === 'grid-area' && !gridAreaKeywords.has(valueLowerCase)) {
  146. return;
  147. }
  148. if (
  149. propLowerCase === 'list-style' &&
  150. !listStyleShorthandKeywords.has(valueLowerCase) &&
  151. !listStyleTypeKeywords.has(valueLowerCase)
  152. ) {
  153. return;
  154. }
  155. if (propLowerCase === 'list-style-type' && !listStyleTypeKeywords.has(valueLowerCase)) {
  156. return;
  157. }
  158. if (optionsMatches(secondaryOptions, 'ignoreKeywords', keyword)) {
  159. return;
  160. }
  161. if (optionsMatches(secondaryOptions, 'ignoreProperties', prop)) {
  162. return;
  163. }
  164. const keywordLowerCase = keyword.toLocaleLowerCase();
  165. let expectedKeyword = null;
  166. /** @type {boolean} */
  167. const camelCaseSvgKeywords =
  168. (secondaryOptions && secondaryOptions.camelCaseSvgKeywords) || false;
  169. if (
  170. primary === 'lower' &&
  171. mapLowercaseKeywordsToCamelCase.has(keywordLowerCase) &&
  172. camelCaseSvgKeywords
  173. ) {
  174. expectedKeyword = mapLowercaseKeywordsToCamelCase.get(keywordLowerCase);
  175. } else if (primary === 'lower') {
  176. expectedKeyword = keyword.toLowerCase();
  177. } else {
  178. expectedKeyword = keyword.toUpperCase();
  179. }
  180. if (keyword === expectedKeyword) {
  181. return;
  182. }
  183. if (context.fix) {
  184. needFix = true;
  185. node.value = expectedKeyword;
  186. return;
  187. }
  188. report({
  189. message: messages.expected,
  190. messageArgs: [keyword, expectedKeyword],
  191. node: decl,
  192. index: declarationValueIndex(decl) + node.sourceIndex,
  193. result,
  194. ruleName,
  195. });
  196. });
  197. if (context.fix && needFix) {
  198. decl.value = parsed.toString();
  199. }
  200. });
  201. };
  202. };
  203. rule.ruleName = ruleName;
  204. rule.messages = messages;
  205. rule.meta = meta;
  206. module.exports = rule;