"use strict" const isWhitespace = require("../../utils/isWhitespace") const report = require("../../utils/report") const ruleMessages = require("../../utils/ruleMessages") const validateOptions = require("../../utils/validateOptions") const balancedMatch = require("balanced-match") const styleSearch = require("style-search") 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