"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;