index.js 6.3 KB

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