index.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. 'use strict';
  2. const declarationValueIndex = require('../../utils/declarationValueIndex');
  3. const keywordSets = require('../../reference/keywordSets');
  4. const optionsMatches = require('../../utils/optionsMatches');
  5. const postcss = require('postcss');
  6. const report = require('../../utils/report');
  7. const ruleMessages = require('../../utils/ruleMessages');
  8. const validateOptions = require('../../utils/validateOptions');
  9. const valueParser = require('postcss-value-parser');
  10. const vendor = require('../../utils/vendor');
  11. const { isNumber } = require('../../utils/validateTypes');
  12. const ruleName = 'time-min-milliseconds';
  13. const messages = ruleMessages(ruleName, {
  14. expected: (time) => `Expected a minimum of ${time} milliseconds`,
  15. });
  16. const meta = {
  17. url: 'https://stylelint.io/user-guide/rules/list/time-min-milliseconds',
  18. };
  19. const DELAY_PROPERTIES = new Set(['animation-delay', 'transition-delay']);
  20. /** @type {import('stylelint').Rule} */
  21. const rule = (primary, secondaryOptions) => {
  22. return (root, result) => {
  23. const validOptions = validateOptions(
  24. result,
  25. ruleName,
  26. {
  27. actual: primary,
  28. possible: isNumber,
  29. },
  30. {
  31. actual: secondaryOptions,
  32. possible: {
  33. ignore: ['delay'],
  34. },
  35. optional: true,
  36. },
  37. );
  38. if (!validOptions) {
  39. return;
  40. }
  41. const minimum = /** @type {number} */ (primary);
  42. root.walkDecls((decl) => {
  43. const propertyName = vendor.unprefixed(decl.prop.toLowerCase());
  44. if (
  45. keywordSets.longhandTimeProperties.has(propertyName) &&
  46. !isIgnoredProperty(propertyName) &&
  47. !isAcceptableTime(decl.value)
  48. ) {
  49. complain(decl);
  50. }
  51. if (keywordSets.shorthandTimeProperties.has(propertyName)) {
  52. const valueListList = postcss.list.comma(decl.value);
  53. for (const valueListString of valueListList) {
  54. const valueList = postcss.list.space(valueListString);
  55. if (optionsMatches(secondaryOptions, 'ignore', 'delay')) {
  56. // Check only duration time values
  57. const duration = getDuration(valueList);
  58. if (duration && !isAcceptableTime(duration)) {
  59. complain(decl, decl.value.indexOf(duration));
  60. }
  61. } else {
  62. // Check all time values
  63. for (const value of valueList) {
  64. if (!isAcceptableTime(value)) {
  65. complain(decl, decl.value.indexOf(value));
  66. }
  67. }
  68. }
  69. }
  70. }
  71. });
  72. /**
  73. * Get the duration within an `animation` or `transition` shorthand property value.
  74. *
  75. * @param {string[]} valueList
  76. * @returns {string | undefined}
  77. */
  78. function getDuration(valueList) {
  79. for (const value of valueList) {
  80. const parsedTime = valueParser.unit(value);
  81. if (!parsedTime) continue;
  82. // The first numeric value in an animation shorthand is the duration.
  83. return value;
  84. }
  85. }
  86. /**
  87. * @param {string} propertyName
  88. * @returns {boolean}
  89. */
  90. function isIgnoredProperty(propertyName) {
  91. if (
  92. optionsMatches(secondaryOptions, 'ignore', 'delay') &&
  93. DELAY_PROPERTIES.has(propertyName)
  94. ) {
  95. return true;
  96. }
  97. return false;
  98. }
  99. /**
  100. * @param {string} time
  101. * @returns {boolean}
  102. */
  103. function isAcceptableTime(time) {
  104. const parsedTime = valueParser.unit(time);
  105. if (!parsedTime) return true;
  106. const numTime = Number(parsedTime.number);
  107. if (numTime <= 0) {
  108. return true;
  109. }
  110. const unit = parsedTime.unit.toLowerCase();
  111. if (unit === 'ms' && numTime < minimum) {
  112. return false;
  113. }
  114. if (unit === 's' && numTime * 1000 < minimum) {
  115. return false;
  116. }
  117. return true;
  118. }
  119. /**
  120. * @param {import('postcss').Declaration} decl
  121. * @param {number} [offset]
  122. * @returns {void}
  123. */
  124. function complain(decl, offset = 0) {
  125. report({
  126. result,
  127. ruleName,
  128. message: messages.expected(minimum),
  129. index: declarationValueIndex(decl) + offset,
  130. node: decl,
  131. });
  132. }
  133. };
  134. };
  135. rule.ruleName = ruleName;
  136. rule.messages = messages;
  137. rule.meta = meta;
  138. module.exports = rule;