UNPKG

6.16 kBJavaScriptView Raw
1// @ts-nocheck
2
3'use strict';
4
5const _ = require('lodash');
6const beforeBlockString = require('../../utils/beforeBlockString');
7const blurComments = require('../../utils/blurComments');
8const hasBlock = require('../../utils/hasBlock');
9const isCustomProperty = require('../../utils/isCustomProperty');
10const isLessVariable = require('../../utils/isLessVariable');
11const isMathFunction = require('../../utils/isMathFunction');
12const keywordSets = require('../../reference/keywordSets');
13const optionsMatches = require('../../utils/optionsMatches');
14const report = require('../../utils/report');
15const ruleMessages = require('../../utils/ruleMessages');
16const styleSearch = require('style-search');
17const validateOptions = require('../../utils/validateOptions');
18const valueParser = require('postcss-value-parser');
19
20const ruleName = 'length-zero-no-unit';
21
22const messages = ruleMessages(ruleName, {
23 rejected: 'Unexpected unit',
24});
25
26function rule(actual, secondary, context) {
27 return (root, result) => {
28 const validOptions = validateOptions(result, ruleName, { actual });
29
30 if (!validOptions) {
31 return;
32 }
33
34 root.walkDecls((decl) => {
35 if (decl.prop.toLowerCase() === 'line-height') {
36 return;
37 }
38
39 const stringValue = blurComments(decl.toString());
40 const ignorableIndexes = new Array(stringValue.length).fill(false);
41 const parsedValue = valueParser(stringValue);
42
43 parsedValue.walk((node, nodeIndex) => {
44 if (decl.prop.toLowerCase() === 'font' && node.type === 'div' && node.value === '/') {
45 const lineHeightNode = parsedValue.nodes[nodeIndex + 1];
46 const lineHeightNodeValue = valueParser.stringify(lineHeightNode);
47
48 for (let i = 0; i < lineHeightNodeValue.length; i++) {
49 ignorableIndexes[lineHeightNode.sourceIndex + i] = true;
50 }
51
52 return;
53 }
54
55 if (node.type !== 'function') {
56 return;
57 }
58
59 const stringValue = valueParser.stringify(node);
60 const ignoreFlag = isMathFunction(node);
61
62 for (let i = 0; i < stringValue.length; i++) {
63 ignorableIndexes[node.sourceIndex + i] = ignoreFlag;
64 }
65 });
66
67 check(stringValue, decl, ignorableIndexes);
68 });
69
70 root.walkAtRules((atRule) => {
71 // Ignore Less variables
72 if (isLessVariable(atRule)) {
73 return;
74 }
75
76 const source = hasBlock(atRule)
77 ? beforeBlockString(atRule, { noRawBefore: true })
78 : atRule.toString();
79
80 check(source, atRule);
81 });
82
83 function check(value, node, ignorableIndexes = []) {
84 if (optionsMatches(secondary, 'ignore', 'custom-properties') && isCustomProperty(value)) {
85 return;
86 }
87
88 const fixPositions = [];
89
90 styleSearch({ source: value, target: '0' }, (match) => {
91 const index = match.startIndex;
92
93 // Given a 0 somewhere in the full property value (not in a string, thanks
94 // to styleSearch) we need to isolate the value that contains the zero.
95 // To do so, we'll find the last index before the 0 of a character that would
96 // divide one value in a list from another, and the next index of such a
97 // character; then we build a substring from those indexes, which we can
98 // assess.
99
100 // If a single value includes multiple 0's (e.g. 100.01px), we don't want
101 // each 0 to be treated as a separate value, possibly resulting in multiple
102 // warnings for the same value (e.g. 0.00px).
103 //
104 // This check prevents that from happening: we build and check against a
105 // Set containing all the indexes that are part of a value already validated.
106 if (ignorableIndexes[index]) {
107 return;
108 }
109
110 const prevValueBreakIndex = _.findLastIndex(value.substr(0, index), (char) => {
111 return [' ', ',', ')', '(', '#', ':', '\n', '\t'].includes(char);
112 });
113
114 // Ignore hex colors
115 if (value[prevValueBreakIndex] === '#') {
116 return;
117 }
118
119 // If no prev break was found, this value starts at 0
120 const valueWithZeroStart = prevValueBreakIndex === -1 ? 0 : prevValueBreakIndex + 1;
121
122 const nextValueBreakIndex = _.findIndex(value.substr(valueWithZeroStart), (char) => {
123 return [' ', ',', ')', '/'].includes(char);
124 });
125
126 // If no next break was found, this value ends at the end of the string
127 const valueWithZeroEnd =
128 nextValueBreakIndex === -1 ? value.length : nextValueBreakIndex + valueWithZeroStart;
129
130 const valueWithZero = value.slice(valueWithZeroStart, valueWithZeroEnd);
131 const parsedValue = valueParser.unit(valueWithZero);
132
133 if (!parsedValue || (parsedValue && !parsedValue.unit)) {
134 return;
135 }
136
137 if (parsedValue.unit.toLowerCase() === 'fr') {
138 return;
139 }
140
141 // Add the indexes to ignorableIndexes so the same value will not
142 // be checked multiple times.
143 _.range(valueWithZeroStart, valueWithZeroEnd).forEach((i) => (ignorableIndexes[i] = true));
144
145 // Only pay attention if the value parses to 0
146 // and units with lengths
147 if (
148 parseFloat(valueWithZero) !== 0 ||
149 !keywordSets.lengthUnits.has(parsedValue.unit.toLowerCase())
150 ) {
151 return;
152 }
153
154 if (context.fix) {
155 fixPositions.unshift({
156 startIndex: valueWithZeroStart,
157 length: valueWithZeroEnd - valueWithZeroStart,
158 });
159
160 return;
161 }
162
163 report({
164 message: messages.rejected,
165 node,
166 index: valueWithZeroEnd - parsedValue.unit.length,
167 result,
168 ruleName,
169 });
170 });
171
172 if (fixPositions.length) {
173 fixPositions.forEach((fixPosition) => {
174 if (node.type === 'atrule') {
175 // Use `-1` for `@` character before each at rule
176 const realIndex =
177 fixPosition.startIndex - node.name.length - node.raws.afterName.length - 1;
178
179 node.params = replaceZero(node.params, realIndex, fixPosition.length);
180 } else {
181 const realIndex = fixPosition.startIndex - node.prop.length - node.raws.between.length;
182
183 node.value = replaceZero(node.value, realIndex, fixPosition.length);
184 }
185 });
186 }
187 }
188 };
189}
190
191function replaceZero(input, startIndex, length) {
192 const stringStart = input.slice(0, startIndex);
193 const stringEnd = input.slice(startIndex + length);
194
195 return `${stringStart}0${stringEnd}`;
196}
197
198rule.ruleName = ruleName;
199rule.messages = messages;
200module.exports = rule;