+++ /dev/null
-"use strict";
-
-const _ = require("lodash");
-const beforeBlockString = require("../../utils/beforeBlockString");
-const hasBlock = require("../../utils/hasBlock");
-const optionsMatches = require("../../utils/optionsMatches");
-const report = require("../../utils/report");
-const ruleMessages = require("../../utils/ruleMessages");
-const styleSearch = require("style-search");
-const validateOptions = require("../../utils/validateOptions");
-
-const ruleName = "indentation";
-const messages = ruleMessages(ruleName, {
- expected: x => `Expected indentation of ${x}`
-});
-
-/**
- * @param {number|"tab"} space - Number of whitespaces to expect, or else
- * keyword "tab" for single `\t`
- * @param {object} [options]
- */
-const rule = function(space, options, context) {
- options = options || {};
-
- const isTab = space === "tab";
- const indentChar = isTab ? "\t" : _.repeat(" ", space);
- const warningWord = isTab ? "tab" : "space";
-
- return (root, result) => {
- const validOptions = validateOptions(
- result,
- ruleName,
- {
- actual: space,
- possible: [_.isNumber, "tab"]
- },
- {
- actual: options,
- possible: {
- except: ["block", "value", "param"],
- ignore: ["value", "param", "inside-parens"],
- indentInsideParens: ["twice", "once-at-root-twice-in-block"],
- indentClosingBrace: [_.isBoolean]
- },
- optional: true
- }
- );
- if (!validOptions) {
- return;
- }
-
- // Cycle through all nodes using walk.
- root.walk(node => {
- const nodeLevel = indentationLevel(node);
- const expectedWhitespace = _.repeat(indentChar, nodeLevel);
-
- let before = node.raws.before || "";
- const after = node.raws.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.)
- const inspectBefore =
- node.root().first === node || before.indexOf("\n") !== -1;
-
- // Cut out any * and _ hacks from `before`
- if (
- before[before.length - 1] === "*" ||
- before[before.length - 1] === "_"
- ) {
- before = before.slice(0, before.length - 1);
- }
-
- // 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
- ) {
- if (context.fix) {
- node.raws.before = fixIndentation(
- node.raws.before,
- expectedWhitespace
- );
- } else {
- report({
- message: messages.expected(legibleExpectation(nodeLevel)),
- node,
- result,
- 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.
- const closingBraceLevel = options.indentClosingBrace
- ? nodeLevel + 1
- : nodeLevel;
- const expectedClosingBraceIndentation = _.repeat(
- indentChar,
- closingBraceLevel
- );
- if (
- hasBlock(node) &&
- after &&
- after.indexOf("\n") !== -1 &&
- after.slice(after.lastIndexOf("\n") + 1) !==
- expectedClosingBraceIndentation
- ) {
- if (context.fix) {
- node.raws.after = fixIndentation(
- node.raws.after,
- expectedClosingBraceIndentation
- );
- } else {
- report({
- message: messages.expected(legibleExpectation(closingBraceLevel)),
- node,
- index: node.toString().length - 1,
- result,
- 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, level) {
- level = level || 0;
-
- if (node.parent.type === "root") {
- return level + getRootBaseIndentLevel(node.parent);
- }
-
- let calculatedLevel;
-
- // 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 (
- optionsMatches(options, "except", "block") &&
- (node.type === "rule" || node.type === "atrule") &&
- hasBlock(node)
- ) {
- calculatedLevel--;
- }
-
- return calculatedLevel;
- }
-
- function checkValue(decl, declLevel) {
- if (decl.value.indexOf("\n") === -1) {
- return;
- }
- if (optionsMatches(options, "ignore", "value")) {
- return;
- }
-
- const declString = decl.toString();
- const valueLevel = optionsMatches(options, "except", "value")
- ? declLevel
- : declLevel + 1;
-
- checkMultilineBit(declString, valueLevel, decl);
- }
-
- function checkSelector(rule, ruleLevel) {
- const 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 (optionsMatches(options, "ignore", "param")) {
- return;
- }
-
- // @nest rules should be treated like regular rules, not expected
- // to have their params (selectors) indented
- const paramLevel =
- optionsMatches(options, "except", "param") || atRule.name === "nest"
- ? ruleLevel
- : ruleLevel + 1;
-
- checkMultilineBit(beforeBlockString(atRule).trim(), paramLevel, atRule);
- }
-
- function checkMultilineBit(source, newlineIndentLevel, node) {
- if (source.indexOf("\n") === -1) {
- return;
- }
-
- // Data for current node fixing
- const fixPositions = [];
-
- // `outsideParens` because function arguments and also non-standard parenthesized stuff like
- // Sass maps are ignored to allow for arbitrary indentation
- let parentheticalDepth = 0;
-
- styleSearch(
- {
- source,
- target: "\n",
- outsideParens: optionsMatches(options, "ignore", "inside-parens")
- },
- (match, matchCount) => {
- const precedesClosingParenthesis = /^[ \t]*\)/.test(
- source.slice(match.startIndex + 1)
- );
-
- if (
- optionsMatches(options, "ignore", "inside-parens") &&
- (precedesClosingParenthesis || match.insideParens)
- ) {
- return;
- }
-
- let expectedIndentLevel = newlineIndentLevel;
-
- // Modififications for parenthetical content
- if (
- !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
- let newlineIndex = match.startIndex;
- if (source[match.startIndex - 1] === "\r") {
- newlineIndex--;
- }
-
- const followsOpeningParenthesis = /\([ \t]*$/.test(
- source.slice(0, newlineIndex)
- );
- if (followsOpeningParenthesis) {
- parentheticalDepth += 1;
- }
-
- const followsOpeningBrace = /\{[ \t]*$/.test(
- source.slice(0, newlineIndex)
- );
- if (followsOpeningBrace) {
- parentheticalDepth += 1;
- }
-
- const startingClosingBrace = /^[ \t]*}/.test(
- source.slice(match.startIndex + 1)
- );
- if (startingClosingBrace) {
- parentheticalDepth -= 1;
- }
-
- expectedIndentLevel += parentheticalDepth;
-
- // Past this point, adjustments to parentheticalDepth affect next line
-
- 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 === node.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
- const afterNewlineSpaceMatches = /^([ \t]*)\S/.exec(
- source.slice(match.startIndex + 1)
- );
-
- if (!afterNewlineSpaceMatches) {
- return;
- }
-
- const afterNewlineSpace = afterNewlineSpaceMatches[1];
- const expectedIndentation = _.repeat(indentChar, expectedIndentLevel);
-
- if (afterNewlineSpace !== expectedIndentation) {
- if (context.fix) {
- // Adding fixes position in reverse order, because if we change indent in the beginning of the string it will break all following fixes for that string
- fixPositions.unshift({
- expectedIndentation,
- currentIndentation: afterNewlineSpace,
- startIndex: match.startIndex
- });
- } else {
- report({
- message: messages.expected(
- legibleExpectation(expectedIndentLevel)
- ),
- node,
- index: match.startIndex + afterNewlineSpace.length + 1,
- result,
- ruleName
- });
- }
- }
- }
- );
-
- if (fixPositions.length) {
- if (node.type === "rule") {
- fixPositions.forEach(function(fixPosition) {
- node.selector = replaceIndentation(
- node.selector,
- fixPosition.currentIndentation,
- fixPosition.expectedIndentation,
- fixPosition.startIndex
- );
- });
- }
-
- if (node.type === "decl") {
- const declProp = node.prop;
- const declBetween = node.raws.between;
-
- fixPositions.forEach(function(fixPosition) {
- if (fixPosition.startIndex < declProp.length + declBetween.length) {
- node.raws.between = replaceIndentation(
- declBetween,
- fixPosition.currentIndentation,
- fixPosition.expectedIndentation,
- fixPosition.startIndex - declProp.length
- );
- } else {
- node.value = replaceIndentation(
- node.value,
- fixPosition.currentIndentation,
- fixPosition.expectedIndentation,
- fixPosition.startIndex - declProp.length - declBetween.length
- );
- }
- });
- }
-
- if (node.type === "atrule") {
- const atRuleName = node.name;
- const atRuleAfterName = node.raws.afterName;
- const atRuleParams = node.params;
-
- fixPositions.forEach(function(fixPosition) {
- // 1 — it's a @ length
- if (
- fixPosition.startIndex <
- 1 + atRuleName.length + atRuleAfterName.length
- ) {
- node.raws.afterName = replaceIndentation(
- atRuleAfterName,
- fixPosition.currentIndentation,
- fixPosition.expectedIndentation,
- fixPosition.startIndex - atRuleName.length - 1
- );
- } else {
- node.params = replaceIndentation(
- atRuleParams,
- fixPosition.currentIndentation,
- fixPosition.expectedIndentation,
- fixPosition.startIndex -
- atRuleName.length -
- atRuleAfterName.length -
- 1
- );
- }
- });
- }
- }
- }
-
- function getRootBaseIndentLevel(node) {
- if (node === root) {
- return 0;
- }
- if (isNaN(node.source.baseIndentLevel)) {
- node.source.baseIndentLevel = getBaseIndentLevel(node, root, space);
- }
- return node.source.baseIndentLevel;
- }
- };
-
- function legibleExpectation(level) {
- const count = isTab ? level : level * space;
- const quantifiedWarningWord = count === 1 ? warningWord : warningWord + "s";
-
- return `${count} ${quantifiedWarningWord}`;
- }
-};
-
-function getBaseIndentLength(indents) {
- return Math.min.apply(Math, indents.map(indent => indent.length));
-}
-
-function getSoftIndentLevel(softIndentCount, space) {
- return Math.round(softIndentCount / (parseInt(space) || 2));
-}
-
-function getBaseIndentLevel(node, root, space) {
- const code = node.source.input.css;
- if (/^\S+/m.test(code)) {
- return 0;
- }
-
- const softIndents = code.match(/^ +(?=\S+)/gm);
- const hardIndents = code.match(/^\t+(?=\S+)/gm);
- let softIndentCount = softIndents ? softIndents.length : 0;
- let hardIndentCount = hardIndents ? hardIndents.length : 0;
-
- if (softIndentCount > hardIndentCount) {
- return getSoftIndentLevel(getBaseIndentLength(softIndents), space);
- } else if (hardIndentCount) {
- return getBaseIndentLength(hardIndents);
- }
-
- let afterEnd = root.nodes[root.nodes.indexOf(node) + 1];
- if (afterEnd) {
- afterEnd = afterEnd.raws.beforeStart;
- } else {
- afterEnd = root.raws.afterEnd;
- }
- hardIndentCount = 0;
- softIndentCount = afterEnd.match(/^\s*/)[0].replace(/\t/g, () => {
- hardIndentCount++;
- return "";
- }).length;
-
- return hardIndentCount + getSoftIndentLevel(softIndentCount, space);
-}
-
-function fixIndentation(str, whitespace) {
- if (!_.isString(str)) {
- return str;
- }
-
- const strLength = str.length;
-
- if (!strLength) {
- return str;
- }
-
- let stringEnd = str[strLength - 1];
-
- if (stringEnd !== "*" && stringEnd !== "_") {
- stringEnd = "";
- }
-
- const stringStart = str.slice(0, str.lastIndexOf("\n") + 1);
-
- return stringStart + whitespace + stringEnd;
-}
-
-function replaceIndentation(input, searchString, replaceString, startIndex) {
- const offset = startIndex + 1;
- const stringStart = input.slice(0, offset);
- const stringEnd = input.slice(offset + searchString.length);
-
- return stringStart + replaceString + stringEnd;
-}
-
-rule.ruleName = ruleName;
-rule.messages = messages;
-module.exports = rule;