text: import {
    isWhitespace,
    report,
    ruleMessages,
    validateOptions,
  } from "../../utils"
  import balancedMatch from "balanced-match"
  import styleSearch from "style-search"
  import valueParser from "postcss-value-parser"
  
  export const ruleName = "function-calc-no-unspaced-operator"
  
  export 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}"`,
  })
  
  export default 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
  }
  