--- /dev/null
+"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;