stylelint
Version:
A mighty, modern CSS linter.
266 lines (225 loc) • 9.61 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.messages = exports.ruleName = undefined;
exports.default = function (space) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var isTab = space === "tab";
var indentChar = isTab ? "\t" : (0, _lodash.repeat)(" ", space);
var warningWord = isTab ? "tab" : "space";
return function (root, result) {
var validOptions = (0, _utils.validateOptions)(result, ruleName, {
actual: space,
possible: [_lodash.isNumber, "tab"]
}, {
actual: options,
possible: {
except: ["block", "value", "param"],
ignore: ["value", "param", "inside-parens"],
indentInsideParens: ["twice", "once-at-root-twice-in-block"],
indentClosingBrace: [_lodash.isBoolean]
},
optional: true
});
if (!validOptions) {
return;
}
// Cycle through all nodes using walk.
root.walk(function (node) {
var nodeLevel = indentationLevel(node);
var expectedWhitespace = (0, _lodash.repeat)(indentChar, nodeLevel);
var before = node.raw("before");
var after = node.raw("after");
// Only inspect the spaces before the node
// if this is the first node in root
// or there is a newline in the `before` string.
// (If there is no newline before a node,
// there is no "indentation" to check.)
var inspectBefore = root.first === node || before.indexOf("\n") !== -1;
// Cut out any * hacks from `before`
before = before[before.length - 1] === "*" || before[before.length - 1] === "_" ? before.slice(0, before.length - 1) : before;
// Inspect whitespace in the `before` string that is
// *after* the *last* newline character,
// because anything besides that is not indentation for this node:
// it is some other kind of separation, checked by some separate rule
if (inspectBefore && before.slice(before.lastIndexOf("\n") + 1) !== expectedWhitespace) {
(0, _utils.report)({
message: messages.expected(legibleExpectation(nodeLevel)),
node: node,
result: result,
ruleName: ruleName
});
}
// Only blocks have the `after` string to check.
// Only inspect `after` strings that start with a newline;
// otherwise there's no indentation involved.
// And check `indentClosingBrace` to see if it should be indented an extra level.
var closingBraceLevel = options.indentClosingBrace ? nodeLevel + 1 : nodeLevel;
if ((0, _utils.hasBlock)(node) && after && after.indexOf("\n") !== -1 && after.slice(after.lastIndexOf("\n") + 1) !== (0, _lodash.repeat)(indentChar, closingBraceLevel)) {
(0, _utils.report)({
message: messages.expected(legibleExpectation(closingBraceLevel)),
node: node,
index: node.toString().length - 1,
result: result,
ruleName: ruleName
});
}
// If this is a declaration, check the value
if (node.value) {
checkValue(node, nodeLevel);
}
// If this is a rule, check the selector
if (node.selector) {
checkSelector(node, nodeLevel);
}
// If this is an at rule, check the params
if (node.type === "atrule") {
checkAtRuleParams(node, nodeLevel);
}
});
function indentationLevel(node) {
var level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
if (node.parent.type === "root") {
return level;
}
var calculatedLevel = void 0;
// Indentation level equals the ancestor nodes
// separating this node from root; so recursively
// run this operation
calculatedLevel = indentationLevel(node.parent, level + 1);
// If options.except includes "block",
// blocks are taken down one from their calculated level
// (all blocks are the same level as their parents)
if ((0, _utils.optionsMatches)(options, "except", "block") && (node.type === "rule" || node.type === "atrule") && (0, _utils.hasBlock)(node)) {
calculatedLevel--;
}
return calculatedLevel;
}
function checkValue(decl, declLevel) {
if (decl.value.indexOf("\n") === -1) {
return;
}
if ((0, _utils.optionsMatches)(options, "ignore", "value")) {
return;
}
var declString = decl.toString();
var valueLevel = (0, _utils.optionsMatches)(options, "except", "value") ? declLevel : declLevel + 1;
checkMultilineBit(declString, valueLevel, decl);
}
function checkSelector(rule, ruleLevel) {
var selector = rule.selector;
// Less mixins have params, and they should be indented extra
if (rule.params) {
ruleLevel += 1;
}
checkMultilineBit(selector, ruleLevel, rule);
}
function checkAtRuleParams(atRule, ruleLevel) {
if ((0, _utils.optionsMatches)(options, "ignore", "param")) {
return;
}
// @nest rules should be treated like regular rules, not expected
// to have their params (selectors) indented
var paramLevel = (0, _utils.optionsMatches)(options, "except", "param") || atRule.name === "nest" ? ruleLevel : ruleLevel + 1;
checkMultilineBit((0, _utils.beforeBlockString)(atRule).trim(), paramLevel, atRule);
}
function checkMultilineBit(source, newlineIndentLevel, node) {
if (source.indexOf("\n") === -1) {
return;
}
// `outsideParens` because function arguments and also non-standard parenthesized stuff like
// Sass maps are ignored to allow for arbitrary indentation
var parentheticalDepth = 0;
(0, _styleSearch2.default)({
source: source,
target: "\n",
outsideParens: (0, _utils.optionsMatches)(options, "ignore", "inside-parens")
}, function (match, matchCount) {
var precedesClosingParenthesis = /^[ \t]*\)/.test(source.slice(match.startIndex + 1));
if ((0, _utils.optionsMatches)(options, "ignore", "inside-parens") && (precedesClosingParenthesis || match.insideParens)) {
return;
}
var expectedIndentLevel = newlineIndentLevel;
// Modififications for parenthetical content
if (!(0, _utils.optionsMatches)(options, "ignore", "inside-parens") && match.insideParens) {
// If the first match in is within parentheses, reduce the parenthesis penalty
if (matchCount === 1) parentheticalDepth -= 1;
// Account for windows line endings
var newlineIndex = match.startIndex;
if (source[match.startIndex - 1] === "\r") {
newlineIndex--;
}
var followsOpeningParenthesis = /\([ \t]*$/.test(source.slice(0, newlineIndex));
if (followsOpeningParenthesis) {
parentheticalDepth += 1;
}
expectedIndentLevel += parentheticalDepth;
if (precedesClosingParenthesis) {
parentheticalDepth -= 1;
}
switch (options.indentInsideParens) {
case "twice":
if (!precedesClosingParenthesis || options.indentClosingBrace) {
expectedIndentLevel += 1;
}
break;
case "once-at-root-twice-in-block":
if (node.parent === root) {
if (precedesClosingParenthesis && !options.indentClosingBrace) {
expectedIndentLevel -= 1;
}
break;
}
if (!precedesClosingParenthesis || options.indentClosingBrace) {
expectedIndentLevel += 1;
}
break;
default:
if (precedesClosingParenthesis && !options.indentClosingBrace) {
expectedIndentLevel -= 1;
}
}
}
// Starting at the index after the newline, we want to
// check that the whitespace characters (excluding newlines) before the first
// non-whitespace character equal the expected indentation
var afterNewlineSpaceMatches = /^([ \t]*)\S/.exec(source.slice(match.startIndex + 1));
if (!afterNewlineSpaceMatches) {
return;
}
var afterNewlineSpace = afterNewlineSpaceMatches[1];
if (afterNewlineSpace !== (0, _lodash.repeat)(indentChar, expectedIndentLevel)) {
(0, _utils.report)({
message: messages.expected(legibleExpectation(expectedIndentLevel)),
node: node,
index: match.startIndex + afterNewlineSpace.length + 1,
result: result,
ruleName: ruleName
});
}
});
}
};
function legibleExpectation(level) {
var count = isTab ? level : level * space;
var quantifiedWarningWord = count === 1 ? warningWord : warningWord + "s";
return count + " " + quantifiedWarningWord;
}
};
var _utils = require("../../utils");
var _lodash = require("lodash");
var _styleSearch = require("style-search");
var _styleSearch2 = _interopRequireDefault(_styleSearch);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var ruleName = exports.ruleName = "indentation";
var messages = exports.messages = (0, _utils.ruleMessages)(ruleName, {
expected: function expected(x) {
return "Expected indentation of " + x;
}
});
/**
* @param {number|"tab"} space - Number of whitespaces to expect, or else
* keyword "tab" for single `\t`
* @param {object} [options]
*/