/
index.js
138 lines (108 loc) · 3.85 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// @ts-nocheck
'use strict';
const balancedMatch = require('balanced-match');
const isWhitespace = require('../../utils/isWhitespace');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const styleSearch = require('style-search');
const validateOptions = require('../../utils/validateOptions');
const valueParser = require('postcss-value-parser');
const ruleName = 'function-calc-no-unspaced-operator';
const messages = ruleMessages(ruleName, {
expectedBefore: (operator) => `Expected single space before "${operator}" operator`,
expectedAfter: (operator) => `Expected single space after "${operator}" operator`,
expectedOperatorBeforeSign: (operator) => `Expected an operator before sign "${operator}"`,
});
function rule(actual) {
return (root, result) => {
const validOptions = validateOptions(result, ruleName, { actual });
if (!validOptions) {
return;
}
function complain(message, node, index) {
report({ message, node, index, result, ruleName });
}
root.walkDecls((decl) => {
valueParser(decl.value).walk((node) => {
if (node.type !== 'function' || node.value.toLowerCase() !== 'calc') {
return;
}
const nodeText = valueParser.stringify(node);
const parensMatch = balancedMatch('(', ')', nodeText);
if (!parensMatch) {
throw new Error(`No parens match: "${nodeText}"`);
}
const rawExpression = parensMatch.body;
const expressionIndex =
decl.source.start.column +
decl.prop.length +
(decl.raws.between || '').length +
node.sourceIndex;
const expression = blurVariables(rawExpression);
checkSymbol('+');
checkSymbol('-');
checkSymbol('*');
checkSymbol('/');
function checkSymbol(symbol) {
const styleSearchOptions = {
source: expression,
target: symbol,
functionArguments: 'skip',
};
styleSearch(styleSearchOptions, (match) => {
const index = match.startIndex;
// Deal with signs.
// (@ and $ are considered "digits" here to allow for variable syntaxes
// that permit signs in front of variables, e.g. `-$number`)
// As is "." to deal with fractional numbers without a leading zero
if ((symbol === '+' || symbol === '-') && /[\d@$.]/.test(expression[index + 1])) {
const expressionBeforeSign = expression.substr(0, index);
// Ignore signs that directly follow a opening bracket
if (expressionBeforeSign[expressionBeforeSign.length - 1] === '(') {
return;
}
// Ignore signs at the beginning of the expression
if (/^\s*$/.test(expressionBeforeSign)) {
return;
}
// Otherwise, ensure that there is a real operator preceding them
if (/[*/+-]\s*$/.test(expressionBeforeSign)) {
return;
}
// And if not, complain
complain(messages.expectedOperatorBeforeSign(symbol), decl, expressionIndex + index);
return;
}
const beforeOk =
(expression[index - 1] === ' ' && !isWhitespace(expression[index - 2])) ||
newlineBefore(expression, index - 1);
if (!beforeOk) {
complain(messages.expectedBefore(symbol), decl, expressionIndex + index);
}
const afterOk =
(expression[index + 1] === ' ' && !isWhitespace(expression[index + 2])) ||
expression[index + 1] === '\n' ||
expression.substr(index + 1, 2) === '\r\n';
if (!afterOk) {
complain(messages.expectedAfter(symbol), decl, expressionIndex + index);
}
});
}
});
});
};
}
function blurVariables(source) {
return source.replace(/[$@][^)\s]+|#{.+?}/g, '0');
}
function newlineBefore(str, startIndex) {
let index = startIndex;
while (index && isWhitespace(str[index])) {
if (str[index] === '\n') return true;
index--;
}
return false;
}
rule.ruleName = ruleName;
rule.messages = messages;
module.exports = rule;