UNPKG

stylelint-scss

Version:

A collection of SCSS specific rules for stylelint

235 lines (215 loc) 7.14 kB
import { utils } from "stylelint" import { atRuleParamIndex, declarationValueIndex, findCommentsInRaws, findOperators, isWhitespace, namespace, } from "../../utils" import mediaQueryParser from "postcss-media-query-parser" export const ruleName = namespace("operator-no-unspaced") export const messages = utils.ruleMessages(ruleName, { expectedAfter: (operator) => `Expected single space after "${operator}"`, expectedBefore: (operator) => `Expected single space before "${operator}"`, }) /** * The actual check for are there (un)necessary whitespaces */ function checkSpaces({ string, globalIndex, startIndex, endIndex, node, result, }) { const symbol = string.substring(startIndex, endIndex + 1) const beforeOk = ( (string[startIndex - 1] === " " && !isWhitespace(string[startIndex - 2])) || newlineBefore(string, startIndex - 1) ) if (!beforeOk) { utils.report({ ruleName, result, node, message: messages.expectedBefore(symbol), index: startIndex + globalIndex, }) } const afterOk = ( (string[endIndex + 1] === " " && !isWhitespace(string[endIndex + 2])) || string[endIndex + 1] === "\n" || string.substr(endIndex + 1, 2) === "\r\n" ) if (!afterOk) { utils.report({ ruleName, result, node, message: messages.expectedAfter(symbol), index: endIndex + globalIndex, }) } } function newlineBefore(str, startIndex) { let index = startIndex while (index && isWhitespace(str[index])) { if (str[index] === "\n") return true index-- } return false } export default function (expectation) { return (root, result) => { const validOptions = utils.validateOptions(result, ruleName, { actual: expectation, }) if (!validOptions) { return } calculationOperatorSpaceChecker({ root, result, checker: checkSpaces, }) } } /** * The core rule logic function. This one can be imported by some other rules * that work with Sass operators * * @param {Object} args -- Named arguments object * @param {PostCSS Root} args.root * @param {PostCSS Result} args.result * @param {function} args.checker -- the function that is run against all the * operators found in the input. Takes these arguments: * {Object} cbArgs -- Named arguments object * {string} cbArgs.string -- the input string (suspected operation) * {number} cbArgs.globalIndex -- the string's index in a global input * {number} cbArgs.startIndex -- the start index of a sybmol to inspect * {number} cbArgs.endIndex -- the end index of a sybmol to inspect * (two indexes needed to allow for `==`, `!=`, etc.) * {PostCSS Node} cbArgs.node -- for stylelint.utils.report * {PostCSS Result} cbArgs.result -- for stylelint.utils.report */ export function calculationOperatorSpaceChecker({ root, result, checker }) { /** * Takes a string, finds all occurencies of Sass interpolaion in it, then * finds all operators inside that interpolation * * @return {array} An array of ojbects { string, operators } - effectively, * a list of operators for each Sass interpolation occurence */ function findInterpolation(string, startIndex) { const interpolationRegex = /#{(.*?)}/g const results = [] // Searching for interpolation let match = interpolationRegex.exec(string) startIndex = !isNaN(startIndex) ? Number(startIndex) : 0 while (match !== null) { results.push({ source: match[0], operators: findOperators({ string: match[0], globalIndex: match.index + startIndex, }), }) match = interpolationRegex.exec(string) } return results } root.walk(item => { let results = [] // Check a value (`10px` in `width: 10px;`) if (item.value !== undefined) { results.push({ source: item.value, operators: findOperators({ string: item.value, globalIndex: declarationValueIndex(item), // For Sass variable values some special rules apply isAfterColon: item.prop[0] === "$", }), }) } // Property name if (item.prop !== undefined) { results = results.concat(findInterpolation(item.prop)) } // Selector if (item.selector !== undefined) { results = results.concat(findInterpolation(item.selector)) } if (item.type === "atrule") { // Media queries if (item.name === "media" || item.name === "import") { mediaQueryParser(item.params).walk(node => { const type = node.type if ([ "keyword", "media-type", "media-feature" ].indexOf(type) !== -1) { results = results.concat(findInterpolation( node.value, atRuleParamIndex(item) + node.sourceIndex) ) } else if ([ "value", "url" ].indexOf(type) !== -1) { results.push({ source: node.value, operators: findOperators({ string: node.value, globalIndex: atRuleParamIndex(item) + node.sourceIndex, isAfterColon: true, }), }) } }) } else { // Function and mixin definitions and other rules results.push({ source: item.params, operators: findOperators({ string: item.params, globalIndex: atRuleParamIndex(item), isAfterColon: true, }), }) } } // All the strings have been parsed, now run whitespace checking results.forEach(el => { // Only if there are operators within a string if (el.operators && el.operators.length > 0) { el.operators.forEach(operator => { checker({ string: el.source, globalIndex: operator.globalIndex, startIndex: operator.startIndex, endIndex: operator.endIndex, node: item, result, }) }) } }) }) // Checking interpolation inside comments // We have to give up on PostCSS here because it skips some inline comments findCommentsInRaws(root.source.input.css).forEach(comment => { const startIndex = comment.source.start + comment.raws.startToken.length + comment.raws.left.length if (comment.type !== "css") { return } findInterpolation(comment.text).forEach(el => { // Only if there are operators within a string if (el.operators && el.operators.length > 0) { el.operators.forEach(operator => { checker({ string: el.source, globalIndex: operator.globalIndex + startIndex, startIndex: operator.startIndex, endIndex: operator.endIndex, node: root, result, }) }) } }) }) }