UNPKG

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