index.js 6.1 KB

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