stylelint
Version:
Modern CSS linter
108 lines (89 loc) • 3.56 kB
JavaScript
import {
isWhitespace,
report,
ruleMessages,
styleSearch,
validateOptions,
} from "../../utils"
import valueParser from "postcss-value-parser"
import balancedMatch from "balanced-match"
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,
outsideFunctionalNotation: true,
}
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`)
if ((symbol === "+" || symbol === "-") && /[\d@\$]/.test(expression[index + 1])) {
const expressionBeforeSign = expression.substr(0, index)
// 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
}