index.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. 'use strict';
  2. const valueParser = require('postcss-value-parser');
  3. const atRuleParamIndex = require('../../utils/atRuleParamIndex');
  4. const declarationValueIndex = require('../../utils/declarationValueIndex');
  5. const report = require('../../utils/report');
  6. const ruleMessages = require('../../utils/ruleMessages');
  7. const { isAtRule } = require('../../utils/typeGuards');
  8. const validateOptions = require('../../utils/validateOptions');
  9. const ruleName = 'number-leading-zero';
  10. const messages = ruleMessages(ruleName, {
  11. expected: 'Expected a leading zero',
  12. rejected: 'Unexpected leading zero',
  13. });
  14. const meta = {
  15. url: 'https://stylelint.io/user-guide/rules/number-leading-zero',
  16. fixable: true,
  17. deprecated: true,
  18. };
  19. /** @type {import('stylelint').Rule} */
  20. const rule = (primary, _secondaryOptions, context) => {
  21. return (root, result) => {
  22. const validOptions = validateOptions(result, ruleName, {
  23. actual: primary,
  24. possible: ['always', 'never'],
  25. });
  26. if (!validOptions) {
  27. return;
  28. }
  29. root.walkAtRules((atRule) => {
  30. if (atRule.name.toLowerCase() === 'import') {
  31. return;
  32. }
  33. check(atRule, atRule.params);
  34. });
  35. root.walkDecls((decl) => check(decl, decl.value));
  36. /**
  37. * @param {import('postcss').AtRule | import('postcss').Declaration} node
  38. * @param {string} value
  39. */
  40. function check(node, value) {
  41. /** @type {Array<{ startIndex: number, endIndex: number }>} */
  42. const neverFixPositions = [];
  43. /** @type {Array<{ index: number }>} */
  44. const alwaysFixPositions = [];
  45. // Get out quickly if there are no periods
  46. if (!value.includes('.')) {
  47. return;
  48. }
  49. valueParser(value).walk((valueNode) => {
  50. // Ignore `url` function
  51. if (valueNode.type === 'function' && valueNode.value.toLowerCase() === 'url') {
  52. return false;
  53. }
  54. // Ignore strings, comments, etc
  55. if (valueNode.type !== 'word') {
  56. return;
  57. }
  58. // Check leading zero
  59. if (primary === 'always') {
  60. const match = /(?:\D|^)(\.\d+)/.exec(valueNode.value);
  61. if (match == null || match[0] == null || match[1] == null) {
  62. return;
  63. }
  64. // The regexp above consists of 2 capturing groups (or capturing parentheses).
  65. // We need the index of the second group. This makes sanse when we have "-.5" as an input
  66. // for regex. And we need the index of ".5".
  67. const capturingGroupIndex = match[0].length - match[1].length;
  68. const index = valueNode.sourceIndex + match.index + capturingGroupIndex;
  69. if (context.fix) {
  70. alwaysFixPositions.unshift({
  71. index,
  72. });
  73. return;
  74. }
  75. const baseIndex = isAtRule(node) ? atRuleParamIndex(node) : declarationValueIndex(node);
  76. complain(messages.expected, node, baseIndex + index);
  77. }
  78. if (primary === 'never') {
  79. const match = /(?:\D|^)(0+)(\.\d+)/.exec(valueNode.value);
  80. if (match == null || match[0] == null || match[1] == null || match[2] == null) {
  81. return;
  82. }
  83. // The regexp above consists of 3 capturing groups (or capturing parentheses).
  84. // We need the index of the second group. This makes sanse when we have "-00.5"
  85. // as an input for regex. And we need the index of "00".
  86. const capturingGroupIndex = match[0].length - (match[1].length + match[2].length);
  87. const index = valueNode.sourceIndex + match.index + capturingGroupIndex;
  88. if (context.fix) {
  89. neverFixPositions.unshift({
  90. startIndex: index,
  91. // match[1].length is the length of our matched zero(s)
  92. endIndex: index + match[1].length,
  93. });
  94. return;
  95. }
  96. const baseIndex = isAtRule(node) ? atRuleParamIndex(node) : declarationValueIndex(node);
  97. complain(messages.rejected, node, baseIndex + index);
  98. }
  99. });
  100. if (alwaysFixPositions.length) {
  101. for (const fixPosition of alwaysFixPositions) {
  102. const index = fixPosition.index;
  103. if (isAtRule(node)) {
  104. node.params = addLeadingZero(node.params, index);
  105. } else {
  106. node.value = addLeadingZero(node.value, index);
  107. }
  108. }
  109. }
  110. if (neverFixPositions.length) {
  111. for (const fixPosition of neverFixPositions) {
  112. const startIndex = fixPosition.startIndex;
  113. const endIndex = fixPosition.endIndex;
  114. if (isAtRule(node)) {
  115. node.params = removeLeadingZeros(node.params, startIndex, endIndex);
  116. } else {
  117. node.value = removeLeadingZeros(node.value, startIndex, endIndex);
  118. }
  119. }
  120. }
  121. }
  122. /**
  123. * @param {string} message
  124. * @param {import('postcss').Node} node
  125. * @param {number} index
  126. */
  127. function complain(message, node, index) {
  128. report({
  129. result,
  130. ruleName,
  131. message,
  132. node,
  133. index,
  134. });
  135. }
  136. };
  137. };
  138. /**
  139. * @param {string} input
  140. * @param {number} index
  141. * @returns {string}
  142. */
  143. function addLeadingZero(input, index) {
  144. // eslint-disable-next-line prefer-template
  145. return input.slice(0, index) + '0' + input.slice(index);
  146. }
  147. /**
  148. * @param {string} input
  149. * @param {number} startIndex
  150. * @param {number} endIndex
  151. * @returns {string}
  152. */
  153. function removeLeadingZeros(input, startIndex, endIndex) {
  154. return input.slice(0, startIndex) + input.slice(endIndex);
  155. }
  156. rule.ruleName = ruleName;
  157. rule.messages = messages;
  158. rule.meta = meta;
  159. module.exports = rule;