index.js 5.1 KB

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