UNPKG

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