index.js 5.0 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 validateOptions = require('../../utils/validateOptions');
  8. const ruleName = 'number-leading-zero';
  9. const messages = ruleMessages(ruleName, {
  10. expected: 'Expected a leading zero',
  11. rejected: 'Unexpected leading zero',
  12. });
  13. const meta = {
  14. url: 'https://stylelint.io/user-guide/rules/list/number-leading-zero',
  15. };
  16. /** @type {import('stylelint').Rule} */
  17. const rule = (primary, _secondaryOptions, context) => {
  18. return (root, result) => {
  19. const validOptions = validateOptions(result, ruleName, {
  20. actual: primary,
  21. possible: ['always', 'never'],
  22. });
  23. if (!validOptions) {
  24. return;
  25. }
  26. root.walkAtRules((atRule) => {
  27. if (atRule.name.toLowerCase() === 'import') {
  28. return;
  29. }
  30. check(atRule, atRule.params);
  31. });
  32. root.walkDecls((decl) => check(decl, decl.value));
  33. /**
  34. * @param {import('postcss').AtRule | import('postcss').Declaration} node
  35. * @param {string} value
  36. */
  37. function check(node, value) {
  38. /** @type {Array<{ startIndex: number, endIndex: number }>} */
  39. const neverFixPositions = [];
  40. /** @type {Array<{ index: number }>} */
  41. const alwaysFixPositions = [];
  42. // Get out quickly if there are no periods
  43. if (!value.includes('.')) {
  44. return;
  45. }
  46. valueParser(value).walk((valueNode) => {
  47. // Ignore `url` function
  48. if (valueNode.type === 'function' && valueNode.value.toLowerCase() === 'url') {
  49. return false;
  50. }
  51. // Ignore strings, comments, etc
  52. if (valueNode.type !== 'word') {
  53. return;
  54. }
  55. // Check leading zero
  56. if (primary === 'always') {
  57. const match = /(?:\D|^)(\.\d+)/.exec(valueNode.value);
  58. if (match === null) {
  59. return;
  60. }
  61. // The regexp above consists of 2 capturing groups (or capturing parentheses).
  62. // We need the index of the second group. This makes sanse when we have "-.5" as an input
  63. // for regex. And we need the index of ".5".
  64. const capturingGroupIndex = match[0].length - match[1].length;
  65. const index = valueNode.sourceIndex + match.index + capturingGroupIndex;
  66. if (context.fix) {
  67. alwaysFixPositions.unshift({
  68. index,
  69. });
  70. return;
  71. }
  72. const baseIndex =
  73. node.type === 'atrule' ? atRuleParamIndex(node) : declarationValueIndex(node);
  74. complain(messages.expected, node, baseIndex + index);
  75. }
  76. if (primary === 'never') {
  77. const match = /(?:\D|^)(0+)(\.\d+)/.exec(valueNode.value);
  78. if (match === null) {
  79. return;
  80. }
  81. // The regexp above consists of 3 capturing groups (or capturing parentheses).
  82. // We need the index of the second group. This makes sanse when we have "-00.5"
  83. // as an input for regex. And we need the index of "00".
  84. const capturingGroupIndex = match[0].length - (match[1].length + match[2].length);
  85. const index = valueNode.sourceIndex + match.index + capturingGroupIndex;
  86. if (context.fix) {
  87. neverFixPositions.unshift({
  88. startIndex: index,
  89. // match[1].length is the length of our matched zero(s)
  90. endIndex: index + match[1].length,
  91. });
  92. return;
  93. }
  94. const baseIndex =
  95. node.type === 'atrule' ? 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 (node.type === 'atrule') {
  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 (node.type === 'atrule') {
  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;