index.mjs 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. import valueParser from 'postcss-value-parser';
  2. import { declarationValueIndex } from '../../utils/nodeFieldIndices.mjs';
  3. import functionArgumentsSearch from '../../utils/functionArgumentsSearch.mjs';
  4. import isStandardSyntaxValue from '../../utils/isStandardSyntaxValue.mjs';
  5. import report from '../../utils/report.mjs';
  6. import ruleMessages from '../../utils/ruleMessages.mjs';
  7. import validateOptions from '../../utils/validateOptions.mjs';
  8. import vendor from '../../utils/vendor.mjs';
  9. const ruleName = 'function-linear-gradient-no-nonstandard-direction';
  10. const messages = ruleMessages(ruleName, {
  11. rejected: 'Unexpected nonstandard direction',
  12. });
  13. const meta = {
  14. url: 'https://stylelint.io/user-guide/rules/function-linear-gradient-no-nonstandard-direction',
  15. };
  16. const LINEAR_GRADIENT_FUNCTION = '(?:-webkit-|-moz-|-o-|-ms-)?linear-gradient';
  17. const LINEAR_GRADIENT_FUNCTION_CALL = new RegExp(`${LINEAR_GRADIENT_FUNCTION}\\(`, 'i');
  18. const LINEAR_GRADIENT_FUNCTION_NAME = new RegExp(`^${LINEAR_GRADIENT_FUNCTION}$`, 'i');
  19. const DIRECTION = /top|left|bottom|right/i;
  20. const DIRECTION_WITH_TO = new RegExp(`^to (${DIRECTION.source})(?: (${DIRECTION.source}))?$`, 'i');
  21. const DIRECTION_WITHOUT_TO = new RegExp(`^(${DIRECTION.source})(?: (${DIRECTION.source}))?$`, 'i');
  22. const DIGIT = /[\d.]/;
  23. const ANGLE = /^[\d.]+(?:deg|grad|rad|turn)$/;
  24. const IN_KEYWORD = /\bin\b/i;
  25. /**
  26. * @param {string} source
  27. * @param {boolean} withToPrefix
  28. */
  29. function isStandardDirection(source, withToPrefix) {
  30. const regexp = withToPrefix ? DIRECTION_WITH_TO : DIRECTION_WITHOUT_TO;
  31. const matches = source.match(regexp);
  32. if (!matches) {
  33. return false;
  34. }
  35. if (matches.length === 2) {
  36. return true;
  37. }
  38. // Cannot repeat side-or-corner, e.g. "to top top"
  39. if (matches.length === 3 && matches[1] !== matches[2]) {
  40. return true;
  41. }
  42. return false;
  43. }
  44. /** @type {import('stylelint').CoreRules[ruleName]} */
  45. const rule = (primary) => {
  46. return (root, result) => {
  47. const validOptions = validateOptions(result, ruleName, { actual: primary });
  48. if (!validOptions) {
  49. return;
  50. }
  51. root.walkDecls((decl) => {
  52. if (!LINEAR_GRADIENT_FUNCTION_CALL.test(decl.value)) return;
  53. valueParser(decl.value).walk((valueNode) => {
  54. if (valueNode.type !== 'function') {
  55. return;
  56. }
  57. functionArgumentsSearch(
  58. valueParser.stringify(valueNode).toLowerCase(),
  59. LINEAR_GRADIENT_FUNCTION_NAME,
  60. (expression, expressionIndex) => {
  61. const args = expression.split(',');
  62. const firstArg = (args[0] || '').trim();
  63. // If the first arg is not standard, return early
  64. if (!isStandardSyntaxValue(firstArg)) {
  65. return;
  66. }
  67. // Skip direction validation for CSS variables
  68. if (firstArg.startsWith('var(')) {
  69. return;
  70. }
  71. // Ignore gradients with modern syntax that have color space interpolation arguments
  72. if (IN_KEYWORD.test(firstArg)) {
  73. return;
  74. }
  75. // If the first character is a number, we can assume the user intends an angle
  76. if (DIGIT.test(firstArg.charAt(0))) {
  77. if (ANGLE.test(firstArg)) {
  78. return;
  79. }
  80. complain();
  81. return;
  82. }
  83. // The first argument may not be a direction: it may be an angle,
  84. // or a color stop (in which case user gets default direction, "to bottom")
  85. // cf. https://drafts.csswg.org/css-images-3/#linear-gradient-syntax
  86. if (!DIRECTION.test(firstArg)) {
  87. return;
  88. }
  89. const withToPrefix = !vendor.prefix(valueNode.value);
  90. if (!isStandardDirection(firstArg, withToPrefix)) {
  91. complain();
  92. }
  93. function complain() {
  94. const index = declarationValueIndex(decl) + valueNode.sourceIndex + expressionIndex;
  95. const endIndex = index + (args[0] || '').trimEnd().length;
  96. report({
  97. message: messages.rejected,
  98. messageArgs: [],
  99. node: decl,
  100. index,
  101. endIndex,
  102. result,
  103. ruleName,
  104. });
  105. }
  106. },
  107. );
  108. });
  109. });
  110. };
  111. };
  112. rule.ruleName = ruleName;
  113. rule.messages = messages;
  114. rule.meta = meta;
  115. export default rule;