| "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}"` |
| }); |
| |
| const rule = function(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 parensMatch = balancedMatch( |
| "(", |
| ")", |
| valueParser.stringify(node) |
| ); |
| 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 preceeding 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; |