UNPKG

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